tier:service member at services/<name>, named @figentra/service-<name>, with its entry at src/main.ts. Services are independent workspace members. Service source holds no Encore files: Encore is the generated runtime target, emitted by stackra gen into .stackra/encore/ (0091, 0124). The generator is not built yet, so the Encore-specific rules below (“when Encore lands”) describe the target, not today’s code.
Layout
pnpm new service <name> renders it (0126):
api.ts, db.ts or encore.service.ts files: endpoints, topics, crons and databases are declared with
decorators in the modules and stackra gen emits the Encore primitives into .stackra/encore/. A new service is a
skeleton with no feature module; src/modules/README.md lists the kind folders.
The eight non-negotiables
1
Placement, and one file per primitive
The placement test passes: a second product with none of this product’s domain would still need
it verbatim, so it belongs in a service named for the capability, not for the product (see
Boundaries). Primitives follow the file layout above.
2
Two-layer tenant isolation, always
Every query runs inside the tenancy helper, and every tenant-owned table has
tenant_id uuid NOT NULL with row-level security enabled and forced. Tenant, scope and actor come only from the
authenticated principal, never from request input. Full rule: Tenancy.
In force from the first tenant-owned table.3
Event-first
Cross-service communication is a versioned event unless the caller needs an authoritative
decision now, and then only through a typed client on a short, closed list of allowed
synchronous calls. A service imports another service’s
Topic and nothing else from it. No
distributed transactions: order steps so a partial failure is safe and reconcilable. See HTTP
and events.4
One gateway
When Encore lands, the single
encore.app is the only gateway and CORS configuration; a service
never sets CORS, and auth: true is the default on every endpoint. Until then a service exposes
no public surface.5
Idempotency on every write
Writes carry an
Idempotency-Key header and the service dedupes for 24 hours per tenant.
Financial writes also run in one transaction and emit an audit event. See HTTP and
events.6
Required files, present and current
Everything in Member standards, plus
isolation.test.ts once a scoped
table exists. AGENTS.md carries a PII table even when it says none.7
Secrets
Application secrets are declared by name (
secret("NAME")) and set by a human; never in a file,
test, log or chat. Tests need zero secrets. See Secrets.8
Definition of done
Code,
pnpm verify green, the service’s own tests green, docs current, reviewer sign-off. A
service is not “done” because it typechecks: pnpm verify never starts a service (see
Testing).Service boundary
- A service owns its entry, its database and migrations, its topics, its public API and its tests.
- No service imports another service’s internals. Cross-service calls go through the generated typed client; shared types go through the producer’s
./contractsentry (ADR 0117). - Reading another service’s database is allowed only read-only, only where an ADR names the pair.
- Cross-service reads: call the owner’s API, or subscribe and keep a small local read model. Cross-service writes: call the owner’s API inside the caller’s flow, or publish a versioned event and let the owner react.
- Service calls are written inside a service directory so the framework can resolve them statically; shared library code takes callbacks instead.
- Data ownership and the ORM: ADR 0014.
Adding a service
pnpm new service <name> --description "<one sentence>"(or--repo <path>for another monorepo).- Fill
README.md,AGENTS.mdand its## PIItable. - Add a feature module under
src/modules/<feature>/and import it from the root module, kind folders only as files appear. - Run
pnpm installthenpnpm verify.