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
| Form | What you write | Examples |
|---|---|---|
| a class you extend | extends Crud(Post) | entity, Crud, Presenter, Collector, Repository, Mirror |
| a value you receive | constructor(posts: RepositoryOf<Post>) | RepositoryOf<E>, Emit<F>, Fact<F>, Facade<H> |
| a call you write | useQuery(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 hold | What you inherit | What you write | |
|---|---|---|---|
entity({…}) | the fields | the type, the judge, the metadata | the field map |
Crud(Post) | an entity | five operations | nothing, or the ones you redefine |
Presenter(Post) | an entity | the target the scan reads | one method per computed field |
Collector(User) | any class | the target the scan reads | collect(ctx) |
Repository(Reading) | one entity, or the several it owns | every gesture of the guarded port — or none, from two on | the queries you name |
Mirror(BookCard) | a shape | the loop, the age, the judge, the write | pull() |
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.