Skip to main content
examples/fullstack-app is generated byte-for-byte from the CLI fullstack preset. The embedded CLI template is the only editable source; run bash scripts/regenerate-fullstack-example.sh --check to detect drift.

Product boundary

The template depends on one auth product: wasi-auth. The former ddd-auth and ddd-authz crates are removed. ddd_cqrs_es remains independent, while the dependency points only from generated DDD consumers to wasi-auth. Requests follow this boundary:
Browser / REST / gRPC
        -> native trusted ingress
        -> VerifiedAuthContext
        -> shared application services
        -> embedded Cedar
        -> PostgreSQL relational auth command kernel
        -> native mail / optional SpiceDB outbox worker
        -> DDD only for application business aggregates
The membership trigger atomically increments the organization authorization revision and emits resource-scoped typed SpiceDB grants/revocations. The native worker preserves per-tuple ordering and carries delivered consistency tokens. SpiceDB remains preview only because its independent latency/soak gate is not yet complete. Embedded Cedar remains the production authorization path. Arbitrary headers cannot construct VerifiedAuthContext. Browser authority comes from a secure host-only HttpOnly cookie; explicit API clients may use bearer tokens. No request accepts admin_token or raw authorization tuples.

Development and production profiles

The generated application uses PostgreSQL with capture mail during local development:
make -C examples/fullstack-app db-up
make -C examples/fullstack-app dev transport=both
make dev installs the exact wasi-auth-outbox-worker release into the example’s target/wasi-auth-tools directory and stops it when Spin exits. HTTP requests commit durable mail intents; the worker marks them delivered. The default capture transport never sends internet email. The registration and resend pages provide an Open captured verification link action after local delivery. Run make outbox-worker and make spin separately only when you need independent process logs. For real local or production delivery through Resend, use a verified sender:
AUTH_MAIL_TRANSPORT=resend
AUTH_RESEND_API_KEY=re_replace_me
AUTH_RESEND_FROM="wasi-auth <auth@example.com>"
These values belong in the native worker environment. The Makefile never adds the Resend API key to Spin variables. Resend delivery uses the outbox correlation ID as the provider idempotency key, so worker retries do not send a second message during the provider’s idempotency window. For local testing, put those values in examples/fullstack-app/.env and run make -C examples/fullstack-app dev. For production, inject them only into the worker service or its secret manager, not into the Spin component.

What the outbox worker is

The outbox worker is not an SMTP server, mail server, or replacement for Resend. It is a native background delivery process:
request -> PostgreSQL transaction
           user state + encrypted mail intent
        -> HTTP response
worker  -> leases pending intent -> calls Resend -> records delivery ID
The application commits the account change and mail intent together. The worker later delivers verification, reset, invitation, and security messages, then records whether each intent was delivered, retried, or dead-lettered. If the worker is down, requests remain usable but email jobs accumulate as pending; restarting the worker drains them. This is why production runs the Spin service and at least one outbox-worker process against the same PostgreSQL database. For local development, make dev owns both processes. Use this only when you need separate logs:
make spin
make outbox-worker
Running only make spin does not send mail. Running only the worker cannot serve pages or API requests. The worker owns Resend and SpiceDB write credentials; the Spin guest receives neither. The stale Spin SQLite migration-only feature was removed before the first RC. It did not implement the product workflows and had already diverged from the relational kernel. The generated identity product now has one authoritative PostgreSQL execution path; a future development adapter must prove complete schema, transaction, and workflow parity before it can re-enter the product. The production profile uses PostgreSQL and the native HTTP-mail worker. The Spin guest never receives mail or SpiceDB write credentials; optional direct checks use a separate check-only token. Startup rejects capture mail, development tools, insecure cookies, a non-HTTPS public base URL, missing CSRF material, development outbox keys, and runtime-generated symmetric signing keys. Ingress signing, credential-vault encryption, outbox encryption, and recovery-code hashing must use distinct production secrets. Production key rings accept ES256 private signing keys and publish only their P-256 public coordinates. wasi-auth rejects RSA/PSS private signing because the transitive RustCrypto RSA implementation is covered by a timing advisory; RS256 is retained only for public-key verification of provider ID tokens. wasi-auth owns the only auth migration source. Development and production execute the same PostgreSQL migration contract. make fresh erases data and the explicit migration step reapplies that canonical schema; the generated app does not maintain a schema copy of its own. The maintained Spin SDK revision declares Rust 1.93 as its MSRV, matching the DDD, wasi-auth, and leptos-wasi-runtime release-candidate graph. The native Spin host is built separately at its truthful Rust 1.94 floor because Wasmtime 46 and Cranelift 0.133 require it.

Release topology

The dependency graph requires a staged RC release: publish leptos-wasi-runtime 0.4.2-rc.1 (aliased as leptos_wasi), then wasi-auth 0.1.0-rc.2, the ddd_cqrs_es 0.3.0-rc.2 library, and finally the ddd-cqrs-es-cli 0.3.0-rc.2 generator and generated consumers. The library and CLI follow wasi-auth because the fullstack preset emits that exact public dependency. Stable releases repeat the same dependency order. The planned leptos_wasi 0.4.0-alpha.3 identifier was superseded: this source history already contains the 0.4.0 and 0.4.1 releases. The final-WASI and islands work therefore continues as 0.4.2-rc.1 instead of moving version history backward.

User journey

Password registration creates a pending user and queues verification mail. The start endpoint never returns a token or verification URL. After verifying, the user accepts an invitation or creates an organization. Organization roles are owner, admin, member, and viewer; custom roles cannot grant ownership or system permissions. The final owner cannot be demoted, removed, or disabled without another owner. Account pages cover profile, passwords, providers, passkeys, MFA assurance, and session revocation. Organization pages cover selection, members, invitations, roles, permissions, and audit activity. System pages cover users, provider and signing-key administration, Cedar policy versions, and configuration health. Sensitive system operations require a system administrator at AAL2.

Transport contract

One Spin HTTP component dispatches ordinary requests to Leptos or REST and gRPC service paths to Tonic. The request and unary gRPC message limit is 256 KiB; authorization batches are limited to 100 checks.
  • auth.v1.AuthService owns credentials, sessions, refresh, logout, and JWKS.
  • organization.v1.OrganizationService owns organizations, invitations, memberships, roles, and permissions.
  • authorization.v1.AuthorizationService exposes bounded checks and provider capabilities only.
  • admin.v1.AdminService owns users, providers, signing keys, policy versions, and health behind AAL2 system authority.
  • audit.v1.AuditService.WatchAuditEvents is cursor based, buffers at most 100 events, reauthorizes every poll, terminates on revocation, and reconnects after five minutes.
The counter example demonstrates unary, server-streaming, client-streaming, and bidirectional-streaming gRPC. Its optional auth feature delegates counter.change and counter.reset checks to the generated fullstack app.

Required verification

Before release, run formatting, warnings-denied Clippy, unit/doc tests, CLI generation and drift checks, the PostgreSQL profile build and live relational contracts, hydrate and split-WASM builds, package dry-runs, and WASM artifact inspection. Live Spin promotion additionally requires browser flows, all four gRPC modes, five paired performance samples, and a ten-minute concurrency-100 soak. Native trusted ingress and embedded Cedar must stay within 10% paired overhead and 25 ms p99 at concurrency 100 respectively. SpiceDB and portable component middleware remain opt-in/experimental until they independently pass those unchanged gates. A compile-only canary is not production support. The 2026-07-12 exact-candidate run passed all five protected-path pairs: native ingress was about 4.83 times faster in the paired median and reduced median p99 by 76.98%. Five 60-second samples reached 24,501.972 median requests/s with 10.847 ms worst p99. The final ten-minute sample reached 24,838.391 requests/s at 10.278 ms p99 with zero status or transport failures, bounded HTTP 401 revocation, no sensitive-log findings, and no second-half RSS growth.