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:
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
Spin is not the mail sender
spin.toml defines the request-facing product only (Leptos UI, REST, gRPC).
Transactional email is not a Spin component. Registration encrypts a mail
intent into PostgreSQL and returns success without calling Resend. Delivery is
owned by the native wasi-auth-outbox-worker process.
AUTH_MAIL_TRANSPORT=resend (or capture) in .env configures the worker.
It does not make Spin send mail. Resend keys never appear in Spin variables.
Local run
The generated application uses PostgreSQL with capture mail during local development: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. Full target list: make -C examples/fullstack-app help
or the example README
(Spin vs outbox worker).
For real local or production delivery through Resend, use a verified sender:
examples/fullstack-app/.env and run
make -C examples/fullstack-app dev (worker required). 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 — deliberately outsidespin.toml so provider secrets stay off the WASM guest and delivery can retry
after the HTTP response:
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 does not send mail (capture or Resend). Running only
the worker cannot serve pages or API requests. The worker owns Resend and
SpiceDB write credentials; the Spin guest receives neither.
Email verification: register vs resend
Registering the same email again never sends another verification mail. A second signup is an email conflict (normalized_email insert-once), not
“resend verification.” That is intentional (no duplicate accounts).
To get another link while pending:
- Open
/verify-email/resend(or the link on/verify-email/pending). - Keep the outbox worker running (
make devoroutbox-worker).
Checklist: same user needs another verify email → resend, not register
again → worker up → still pending → under 5 resends/hour.
Full operator detail (UI paths, token invalidation on resend, errors for spent
links):
Email verification: register vs resend.
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. Run the cross-repository gate from the DDD checkout: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.7 library, and finally the
ddd-cqrs-es-cli 0.3.0-rc.7 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 areowner, 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.AuthServiceowns credentials, sessions, refresh, logout, and JWKS.organization.v1.OrganizationServiceowns organizations, invitations, memberships, roles, and permissions.authorization.v1.AuthorizationServiceexposes bounded checks and provider capabilities only.admin.v1.AdminServiceowns users, providers, signing keys, policy versions, and health behind AAL2 system authority.audit.v1.AuditService.WatchAuditEventsis cursor based, buffers at most 100 events, reauthorizes every poll, terminates on revocation, and reconnects after five minutes.
auth feature delegates
counter.change and counter.reset checks to the generated fullstack app.