Skip to main content
A service is a 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):
There are no 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 ./contracts entry (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

  1. pnpm new service <name> --description "<one sentence>" (or --repo <path> for another monorepo).
  2. Fill README.md, AGENTS.md and its ## PII table.
  3. Add a feature module under src/modules/<feature>/ and import it from the root module, kind folders only as files appear.
  4. Run pnpm install then pnpm verify.

What enforces this