The base

The base of a Fougere application is not what the framework ships. It is the entity you wrote.

Everything Fougere hands back is indexed by that declaration, and it comes in three forms. Knowing the three is enough to predict where the schema goes in an API you have never seen: between the parentheses of a class you extend, in the type of a value you receive, or as the first argument of a call you write.

Three forms, one parameter

FormWhat you writeExamples
a class you extendextends Crud(Post)entity, Crud, Presenter, Collector, Repository, Mirror
a value you receiveconstructor(posts: RepositoryOf<Post>)RepositoryOf<E>, Emit<F>, Fact<F>, Facade<H>
a call you writeuseQuery(Post, 'list')useQuery, useCommand, useFormFor

The parameter is always something you declared yourself — usually an entity, sometimes a fact or a handler. Nothing in that column is a string, a name, or a key registered elsewhere: rename the class and the compiler follows every reader of it.

A class you extend

Six of them. What varies is not their nature but how much your declaration determines — Collector(User) can derive almost nothing from "I resolve a User", while Crud(Post) derives five whole operations from Post's table.

The parentheses holdWhat you inheritWhat you write
entity({…})the fieldsthe type, the judge, the metadatathe field map
Crud(Post)an entityfive operationsnothing, or the ones you redefine
Presenter(Post)an entitythe target the scan readsone method per computed field
Collector(User)any classthe target the scan readscollect(ctx)
Repository(Reading)one entity, or the several it ownsevery gesture of the guarded port — or none, from two onthe queries you name
Mirror(BookCard)a shapethe loop, the age, the judge, the writepull()

Only the first is mandatory. A handler that extends nothing is an ordinary handler; a repository you never write still resolves. Where extending is the declaration — presenters, collectors — the base carries a target and no behaviour at all.

Each of them is documented where it is used, and the links above go there. One has no page of its own, because it belongs to no single step: it is the base itself.

Mirror — a local copy of rows you cannot query

A real database is queried where it is. Mirror(Shape) is the answer for a source with no algebra: an HTTP API, a partner's catalog, a Frond behind a wire. The subclass supplies one thing, the pull:

// services/PartnerCatalog.ts
export default class PartnerCatalog extends Mirror(BookCard) {
  async *pull(since?: Date) {
    for (let page = 0; page !== null; ) {
      const body = await fetch(`${api}?page=${page}&since=${since?.toISOString() ?? ''}`);
      const { items, next } = await body.json();
      yield items.map(toCard);
      page = next;
    }
  }
}

Everything around it is the same for every mirror and lives in the base: refresh() reads the high-water mark, pulls from there, judges each page and writes it. The shape must carry an updated() field — a copy that cannot say when it was pulled reads exactly like live rows — and Mirror refuses at construction otherwise.

A page is client input, so it meets the same judge as any other, strictly: a partner renaming label writes nulls in silence otherwise. A refused page stops the pass; what earlier pages wrote stays, because an upsert is idempotent and the next run resumes from the mark.

It does not schedule itself. A boot hook, a cron, an operation behind a door are all legitimate and none of them is this class's business.

Mirror has no directory of its own in the Frond vocabulary yet — unlike presenters/ and collectors/, a mirrors/ folder is not scanned. Place it where providers are read (services/ or repositories/). As a storage holder, it is handed the Storage its prefab was built on; handlers, presenters and collectors do not have that exception.

A value you receive

The second form is a constructor parameter, resolved by type — never by parameter name, never by a string key:

export default class PostHandler {
  constructor(
    private posts: RepositoryOf<Post>,
    private announce: Emit<PostPublished>,
  ) {}
}

RepositoryOf<E> is the default repository shape: it forwards the guarded storage port and resolves even when no repository file exists. Injecting Storage<E> directly into a handler, presenter or collector is refused at boot; name storage access through a repository. Emit<F> announces a fact, and accepting a Fact<F> is the subscription — no topic, no register call. Facade<H> is the one whose parameter is not an entity: it names another Frond's handler, which is a declaration of yours all the same.

A call you write

The third form is the client side, and the entity is the first argument:

const { items } = await useQuery(Post, 'list');
const publish = useCommand(Post, 'publish');
const form = useFormFor(Post, { op: 'update', params: { id } });

Same designation as the server, so the local judge is the remote judge and a form contract is not a second declaration of the entity.

One exception worth stating: useCurrentUser() takes no argument. It reads its entity from the session rather than from a call site.

Why a class, and never a decorator

A decorator would have to modify the runtime to attach the storage, the target, or the five operations. extends states the same thing in the language: the type follows without a second declaration, getFields() is a plain static, and nothing is registered behind your back.

It also survives where a scan cannot reach. Crud declares its operations at runtime, on the class, so an installed Frond whose source no parser can open still publishes its contract. Decorator metadata would need a compiler flag and a reflection library to say as much, and would say it only where both were configured.

Next: Entities.

Built with Fougere — this site runs on the framework it documents.