A complete, working reference implementation for bootstrapping new Underlay-based projects. Copy these files and rename acme to your project name.
underlay-reference/
├── apps/
│ ├── acme-api/ # Rust backend (API server + background jobs)
│ ├── acme-admin/ # SvelteKit admin frontend
│ └── acme-front/ # SvelteKit public frontend
├── packages/
│ ├── acme-client/ # TypeScript API client library
│ └── acme-ui/ # Shared UI components
├── docs/ # Documentation authority
├── config/ # Workspace-root config stack
├── package.json # Root Bun workspace manifest
├── bun.lock # One root lockfile
└── effigy.toml # Root Effigy catalog
One Git repository owns the whole workspace. Runtime applications live under
apps/*, reusable internal libraries under packages/*, and docs authority is
root docs/. Names stay product-specific; the role map above is the contract.
The JavaScript workspace is declared once in the root package.json
(apps/acme-admin, apps/acme-front, packages/acme-client,
packages/acme-ui) with one root bun.lock and no child lockfiles. Internal
package edges use workspace:*. apps/acme-api is Rust-only and keeps its own
app-local Cargo workspace, so it is not a JavaScript workspace member.
Committed application dependencies resolve Underlay from the released Git tag
(v0.9.5 at time of writing), not from sibling source paths.
Reference-app planning and architecture live in docs/.
- Start with
docs/README.md - Use
docs/vision/001-acme-reference-implementation-vision.mdfor the long-term role of the repo - Use
docs/architecture/000-overview.mdfor the package map and system layout - Use
docs/architecture/product-guardrails.mdfor the active retained-surface guardrails - Use
docs/policy/001-working-rules.mdfor the active strict execution rules - Use
docs/processes/210-reference-implementation-notes.mdfor implementation notes and validation commands
AGENTS.md files in this repository are intentionally kept lean and point back to that docs authority.
Use Effigy as the default command surface from the workspace root:
effigy tasks
effigy workspace:js:prepare
effigy health
effigy validateworkspace:js:prepare is the one frozen root install
(bun install --frozen-lockfile) for the whole JavaScript workspace. Do not run
per-package installs.
effigy test --plan prints the resolved plan before anything runs — the
targets, the suite chosen per target, and the evidence Effigy used to choose
it. Read it first when test shape matters:
effigy test --plan
effigy test
effigy test acme-apieffigy test runs three targets: acme-api on the configured rust suite
(cargo test --workspace), and acme-admin and acme-client on vitest.
acme-front appears in the plan's target summary with available-suites: vitest but no default suite and command: <none>. That is deliberate —
apps/acme-front/effigy.toml sets [test.suites.vitest] default = false, so
its currently empty suite does not fail the root test / validate / qa
sequences. Run it explicitly with effigy acme-front/test once it has tests.
The plan header reports targets: 3 for that reason; read it rather than
counting summary rows.
Sibling underlay and poodle are excluded from the root plan.
Database-backed Rust tests skip themselves unless DATABASE_URL or
TEST_DATABASE_URL is set, so a plain effigy test stays useful without a
running stack.
effigy qa:conformanceRuns the two released Underlay checkers — workspace shape and env/secret
authority — from the installed @inflatable-cookie/underlay package. They read
this repository only; no sibling Underlay checkout is required.
First-time bring-up from another directory:
effigy bootstrap git@github.com:inflatable-cookie/underlay-reference.git
effigy bootstrap git@github.com:inflatable-cookie/underlay-reference.git --startCommon workspace commands:
effigy dev
effigy qa
effigy qa:docs
effigy qa:northstar
effigy state plan
effigy state apply local --yes
effigy acme-api/migration:resetRoot effigy state plan / effigy state apply local --yes orchestrate the local schema and dev-overlay stack. Package-owned migration:* tasks stay in apps/acme-api/ and resolve through child-catalog routing from the workspace root.
- shared non-secret behavior belongs in the workspace-root config stack:
config/default.tomlplusconfig/effigy.toml(shared dev-stack overlay, loaded whenENVIRONMENT=effigy) plus optionalconfig/local.toml config/local.tomlis for personal, machine-local overrides only — it layers last, so anything duplicated fromeffigy.tomlsilently wins on your machine. After pulling the config convergence (2026-08), strip existinglocal.tomlfiles back to personal tweaks; the shared dev-stack config now lives in the committedconfig/effigy.tomlapps/acme-admin/andapps/acme-front/generate public runtime config from the root stack rather than reading.envfiles- true secrets move through Effigy-managed runtime injection or the local
secrets vault, not committed or ad hoc
.envfiles
config/required-secrets.txt is the human-readable authority; root
effigy.toml makes it executable. effigy secrets list shows which keys are
unconditional and where each is injected:
effigy secrets init # create the encrypted local vault (once per clone)
effigy secrets list # declared keys, targets, and required flags
effigy secrets doctor # declaration/backend check; never reads values
effigy secrets set <name> # store one value; prompts, never echoesThe vault lives at .effigy/secrets/local.vault and is gitignored. Values are
never printed and never committed.
auth_jwt_private_key and auth_jwt_public_key are required = true: the API
refuses to start without them in every environment. You do not normally set
them by hand — the Effigy bundle's generate hook fills the generatable dev keys
on first secrets-required task startup, so effigy dev works from a clean
clone.
Three keys are deliberately not required = true, each for a stated reason:
| Key | Why not required | What is required |
|---|---|---|
database_url |
local and effigy resolve it from the committed non-secret config/effigy.toml overlay, so the dev loop needs no vault value |
deployed environments must inject it as a real secret |
encryption_key |
may be absent with an explicit warning in local, effigy, and test | required in deployed environments; startup fails without it |
| provider credentials | SMTP, SES/AWS, Google OAuth, and object-store keys are conditional | required only once the matching backend or adapter is selected |
Effigy's required flag is a single boolean and cannot express those
conditions, so the condition is recorded in each declaration's description
and in config/env-manifest.txt. Marking them required would block the local
loop and claim unused providers are mandatory.
Bootstrap notes:
effigy bootstrap ...clones the reference workspace and applies the repo-owned[bootstrap]contract- setup starts the workspace container and runs one frozen root workspace install; there is no per-package install step
- the Effigy bundle may mount sibling
../underlayand../poodlefor local framework development, cross-repo QA scripts, and docs — those mounts are not the committed application dependency source - add
--startwhen you want it to launch the rootdevstack after setup
The reference template consumes Underlay from the released Git repository:
"@inflatable-cookie/underlay": "git+ssh://git@github.com/inflatable-cookie/underlay.git#v0.9.5"underlay-core = { git = "ssh://git@github.com/inflatable-cookie/underlay.git", tag = "v0.9.5" }Poodle core/Svelte packages resolve from the public npm registry at 0.2.2.
For lockstep Underlay framework development inside this workspace, Effigy may
still mount a sibling ../underlay checkout. Use effigy deps link when you
need to temporarily point Cargo or Bun back at that checkout; restore the tagged
dependencies before opening a consumer adoption PR.
Full-featured API server with:
- Authentication: JWT tokens, password auth, TOTP 2FA, passkeys, email verification
- Session Management: Token refresh, fingerprint validation, session listing/revocation
- Database: SQLx with migrations, connection pooling
- Background Jobs: Underlay jobs system integration
- Email: Template-based emails routed through SMTP and Mailpit in local dev
- Media Library: File uploads with versioning, deduplication, and blob storage
- API Structure: Explicit runtime, shared, front, and admin route families
Route families and their access posture:
| Family | Source | Paths | Posture |
|---|---|---|---|
| runtime | routes/runtime.rs |
/v1/health, /favicon.ico, /api/openapi.json, /api/docs |
unauthenticated, no CSRF, never requires X-Api-Version |
| shared | routes/shared/router.rs |
/v1/auth/*, /v1/account/* |
mixed bootstrap and authenticated; CSRF on cookie-backed mutations |
| front | routes/front/router.rs |
/v1/projects/* |
authenticated product-user routes |
| admin | routes/admin/router.rs |
/v1/admin/* |
AdminUser gate |
The OpenAPI document is served at /api/openapi.json with Swagger UI at
/api/docs. Both belong to the runtime family and are exposed only in
development environments — main.rs passes
app_config.env.is_development() into the router builder, so staging,
production, and any unrecognised environment name serve neither. Changing
that is a deployment policy decision, not a route change.
Business endpoints are path-versioned under /v1/*. This app has declared the
optional X-Api-Version header: the TypeScript client sends it on every
request and the server validates it across all three business families. Runtime
endpoints are exempt by contract.
Crate organization:
core- Domain primitives, error types, UUID helpersinfra- Configuration, logging, email setupdb- Database pool, migrations, query functionsauth- Authentication service, JWT handling, 2FAdomain- Business logic (minimal example)jobs- Background job handlersapi- HTTP handlers, routes, server setup
API client library for frontend apps:
- Typed API commands (auth, account, health, media)
- Automatic token refresh with AuthManager
- HTTP client with request/response interceptors
- Cookie-based or in-memory token storage
Admin dashboard frontend:
- Auth pages (login, forgot password)
- Account management (profile, password change, 2FA, passkeys)
- Protected route layout with auth guards
- Underlay UI Kit integration
- Admin detail endpoints use
ETagwithCache-Control: private, no-cache, must-revalidate. - Admin detail GET requests support
If-None-Matchand can return304. - Admin edit/update endpoints support
If-Matchoptimistic concurrency and return412(resource.precondition_failed) on stale updates. - Admin edit UIs reload latest server state on
412and ask users to reapply edits.
Public-facing frontend:
- Landing page
- Auth integration ready
- SSR-compatible setup
Use the Effigy-owned path. The older raw Docker and localhost setup flow is not the supported bootstrap model for this workspace anymore.
effigy bootstrap git@github.com:inflatable-cookie/underlay-reference.gitAdd --start if you want the dev stack to launch after setup.
effigy workspace:js:prepare
effigy health
effigy validate
effigy devworkspace:js:prepare is the single frozen root workspace install. There is no
per-package install step.
# Full workspace: shell + admin + front + api + jobs + managed containers
effigy dev| Service | URL |
|---|---|
| Front | https://acme.test |
| Admin | https://admin.acme.test |
| API | https://api.acme.test |
| pgweb | https://pgweb.acme.test |
| Mailpit | https://mailpit.acme.test |
| MinIO Console | https://minio.acme.test |
Notes:
effigy devis the only supported workspace dev runner.effigy devstarts one canonicalworkspacecontainer and runs shell, API, jobs, front, and admin inside it.- The managed shell tab opens at the workspace root inside that running
workspacecontainer. - The local shape is domain-first through the Effigy gateway: HTTPS for front/admin/API/pgweb/Mailpit/MinIO Console, plus
https://s3.acme.testfor browser-facing S3 uploads. - Use the same
.testaliases inside the workspace container too:db.acme.test,smtp.acme.test,s3.acme.test. - Postgres persists in the repo-scoped named volume
underlay-reference-dev-postgres-data; MinIO usesunderlay-reference-dev-minio-data. - Older host bind-mount paths under
.effigy/runtime/data/are not migrated automatically into those named volumes.
These tasks are provided by the Effigy underlay bundle, so this repo
does not carry its own error-reporting script.
After acme-api is running, run:
effigy smoke:error-loggingThis triggers a forced ApiError at POST /v1/dev/error-smoke against https://api.acme.test (debug builds only), then verifies the latest platform.error_log row includes:
error_codemessagecontext.handler_context
To measure current handler_context null-rate (default last 24h):
effigy metrics:error-logTo run the full validation sequence (route checks + smoke + null-rate metrics):
effigy validate:error-reportingTo create a new project from this reference:
mkdir my-project && cd my-project
mkdir -p apps packages
cp -r /path/to/underlay-reference/apps/acme-api ./apps/api
cp -r /path/to/underlay-reference/apps/acme-admin ./apps/admin
cp -r /path/to/underlay-reference/apps/acme-front ./apps/front
cp -r /path/to/underlay-reference/packages/acme-client ./packages/api-client
cp -r /path/to/underlay-reference/packages/acme-ui ./packages/ui
cp /path/to/underlay-reference/package.json ./package.jsonKeep the apps/* and packages/* split. Do not flatten packages back to the
repository root.
Replace acme with your project name throughout:
| Pattern | Replace With |
|---|---|
acme-api, acme-client, etc. |
myapp-api, myapp-client, etc. |
acme_* (package/crate names) |
myapp_* |
acme_access_token |
myapp_access_token |
acme_refresh_token |
myapp_refresh_token |
AcmeLocalAuthService |
MyAppLocalAuthService |
configureAcmeClient |
configureMyAppClient |
ACME_* env vars |
MYAPP_* |
Keep the workspace-level bootstrap assumptions aligned when you rename the reference:
- root
package.jsonworkspacespaths andpackageManagerpin - root
effigy.tomlcatalog.alias - root
effigy.toml[bundle.dirs]physical package paths - root
effigy.toml[containers.stack]profile,project_name, anddns.domain - root
effigy.tomlready_message - child
effigy.tomlaliases where package names change - regenerate the single root
bun.lock; do not add child lockfiles
When you need a different Underlay release, update the tag in every web manifest
and apps/acme-api/Cargo.toml workspace dependency, then regenerate the Bun and
Cargo locks narrowly.
acme-api uses layered config precedence (repo-root config/ stack):
config/default.toml(committed, all environments)config/<environment>.toml(env-named overlay —config/effigy.tomlfor the shared dev stack)config/local.toml(personal overrides, gitignored)- allowlisted environment variables (secrets and runtime wiring)
Use TOML for app behavior defaults, and env vars for secrets and runtime
wiring. .env files are not part of the runtime contract: there is no
.env, .env.local, or .env.example in the target posture, and nothing in
this workspace reads one.
Two tracked files are the env authority:
config/env-manifest.txt— the complete environment surface any runtime process may read, with each key's condition recorded inlineconfig/required-secrets.txt— the startup-critical subset
Both are static key inventories. They never carry values; secret presence stays
an operator and runtime concern. effigy qa:conformance proves they exist,
parse, and agree.
For the current underlay-reference local shape:
- keep non-secret dev values in the committed root
config/stack - keep personal, machine-local overrides in
config/local.toml - keep local secrets in the Effigy vault, injected at task/container runtime
- keep
apps/acme-api/effigy.tomlas plain task orchestration (cargo run ...), not an env dump
ENVIRONMENT selects both the behavior class and the config overlay. An unset
or unrecognised name fails closed to deployed production behavior.
| Class | Names | Posture |
|---|---|---|
| Non-deployed | local, effigy, test |
dev seeds, CORS origin mirroring, and bounded startup warnings are allowed |
| Deployed | dev, staging, production |
fail closed: malformed config, COOKIE_SECURE=false, and CSRF disablement are startup errors |
AUTH_JWT_PRIVATE_KEY=... # Required everywhere; generated by generate-jwt-env
AUTH_JWT_PUBLIC_KEY=... # Required everywhere; generated by generate-jwt-env
DATABASE_URL=postgres://postgres:postgres@db.acme.test:5432/acme
# Deployed only: local/effigy take it from config/effigy.toml
ENCRYPTION_KEY=... # Deployed only: warns in local/effigy/testOnly the JWT pair is required in every environment. DATABASE_URL and
ENCRYPTION_KEY are startup-critical but conditional — see the Secrets
Bootstrap table above and config/required-secrets.txt, which state each
condition.
ENVIRONMENT=local|effigy|test|dev|staging|production
HOST=0.0.0.0
PORT=41001
PUBLIC_HOST=api.acme.test
RUST_LOG=debug
CORS_ORIGINS=https://acme.test,https://admin.acme.test
COOKIE_DOMAIN=.acme.test
COOKIE_SECURE=true
EMAIL_ADAPTER=noop|smtp|ses
SMTP_HOST=smtp.acme.test
SMTP_PORT=1025
SMTP_TLS=none
ACME_S3_BUCKET=acme-media
ACME_S3_ENDPOINT=http://s3.acme.test:9000
ACME_S3_PUBLIC_URL_BASE=https://s3.acme.test/acme-mediaEverything else, including the conditional Redis, SES/AWS, OAuth, and
trusted-proxy keys, is listed with its condition in
config/env-manifest.txt. Do not add a runtime env read without adding it
there.
- Define entities in
apps/api/crates/domain/src/ - Add database tables in
apps/api/migrations/ - Create query functions in
apps/api/crates/db/src/ - Add API routes in
apps/api/crates/api/src/routes/ - Add client commands in
packages/api-client/src/commands/ - Build UI in
apps/admin/andapps/front/
See the Underlay docs for detailed patterns:
- LLM Bootstrap Guide - step-by-step bootstrap rules
- Rust Backend Guide - API patterns
- TypeScript Client Guide - client patterns
- Admin Guide - admin frontend patterns
- Frontend Guide - public frontend patterns