Skip to content

Latest commit

Β 

History

History
356 lines (285 loc) Β· 26.5 KB

File metadata and controls

356 lines (285 loc) Β· 26.5 KB

fuz_app

fullstack app library β€” auth, sessions, accounts, DB, SSE, route specs, CLI infrastructure

NOTE: AI-generated

For coding conventions, see Skill(fuz-stack). Commit policy: see workspace CLAUDE.md (this repo is in git_commit_deny).

Cleanest architecture takes priority

When two designs are on the table β€” one narrow and one with cleaner layering β€” choose the cleaner one even when it costs churn or breakage. Layered shapes (e.g. domain code that returns {status, body} and lets each transport bind, vs. domain code emitting transport-shaped responses in-line) compound across consumers and time; "narrow diff" reasoning ships drift to every dispatcher and test that extends the surface later. Pay the churn once at the source. Sample applications: the dispatcher authorization phase fold (auth-domain {status, body} β†’ transport-bound responses) and most other refactors that touch a shared boundary.

Test/prod write-semantics parity

Defense-in-depth checks that exist to compensate for divergent write semantics between test helpers and production are smells. When you find one, ask whether the test helper should mirror prod state instead β€” most of the time the redundant check is doing real work for the wrong reason. Better: align write semantics so production code can trust a single signal. If a test path genuinely must diverge from prod (cost, scope), document the divergence at the symbol level (_unscoped, _direct, TEST_CONTEXT_PRESET_KEY) so the redundancy is explicit, not load-bearing. The bootstrap case in auth/bootstrap_account.ts is the canonical example β€” a query_account_has_any check inside the bootstrap transaction existed to defend against the test helper leaving bootstrap_lock unflipped while still inserting an account; teaching the test helper (bootstrap_test_keeper) to flip the lock the same way production does made the redundant check droppable and let production code trust the lock as the single signal.

  • ./docs/identity.md β€” Auth design rationale
  • ./docs/security.md β€” Security properties and deployment
  • ./docs/architecture.md β€” DB, session, error schema, subsystem details
  • ./docs/usage.md β€” Code examples (routes, server, SSE, action specs)
  • ./docs/testing.md β€” Consumer test suite wiring guide
  • ./docs/local-daemon.md β€” PGlite local daemon pattern

Quick Reference

Standard gro commands apply (see Skill(fuz-stack)). Never run gro dev β€” the user manages the dev server.

After Changing fuz_app Source

Consumer projects import from dist/ via .js specifiers. After modifying fuz_app source, run gro build before consumers can see the changes:

cd ~/dev/fuz_app && gro build    # rebuild dist/ with updated types
cd ~/dev/{consumer} && gro check --build --no-lint --no-gen   # check consumer

Consumers use --no-lint --no-gen because lint and gen are fuz_app-local concerns.

Symlink Recipe for Cross-Repo Iteration

When iterating across fuz_app + a consumer locally, symlink the fuz_app package root into the consumer's node_modules β€” not fuz_app's source directory or dist/:

# Correct
ln -s ~/dev/fuz_app ~/dev/{consumer}/node_modules/@fuzdev/fuz_app

The exports map in fuz_app's package.json references ./dist/* from the package root β€” symlinking the root makes consumer imports resolve to the freshly-built dist/ after each gro build. Symlinking source or dist/ directly breaks resolution and surfaces as missing-export errors.

Dual-resolution noise. Even with the correct symlink, TypeScript's strict structural identity surfaces "Two different types with this name exist" errors when fuz_app's nested node_modules/@fuzdev/fuz_util (or hono) gets reached separately from the consumer's top-level copy. The errors look like Type 'Logger' is not assignable to type 'Logger' with paths differing only in nesting depth. Resolution: dedupe by symlinking fuz_util into fuz_app's node_modules too β€”

cd ~/dev/fuz_app && npm link @fuzdev/fuz_util

β€” so consumer imports and fuz_app imports resolve to the same on-disk copy. See visionesdelcaribe.org's CLAUDE.md Β§"Cross-repo work via local npm link" for the full triple-link pattern across fuz_app + fuz_util + consumer. Tests are the load-bearing signal when the typecheck step shows this noise; once the symlink layout converges, both typecheck and test pass.

Deno + nodeModulesDir: "manual" rejects version-skewed symlinks. Deno's manual mode reconciles node_modules against the consumer's declared range. If the local fuz_app's version doesn't satisfy the consumer's ^x.y.z constraint, Deno errors with "Could not find @fuzdev/fuz_app in a node_modules folder" β€” not because the symlink itself is rejected, but because the version doesn't match. The fix is to bump the consumer's package.json + deno.json constraints to admit the locally-published version, then re-run npm install. The symlink itself works fine once versions align; the gro transitive oxc-parser also forces --allow-ffi on the Deno binary, and test-binary scenarios that spawn shell commands need --allow-run (verified in zzz's cross-backend test config).

Library Modules

fuz_app uses deep path imports β€” no barrel/index exports. The wildcard exports in package.json ("./*.js") makes every module in dist/ importable:

import { create_app_server } from '@fuzdev/fuz_app/server/app_server.ts';
import { create_session_config } from '@fuzdev/fuz_app/auth/session_cookie.ts';
import type { RouteSpec } from '@fuzdev/fuz_app/http/route_spec.ts';

Dense subsystems have nested CLAUDE.md β€” consult those when working in that subtree.

Dense subsystems (see nested CLAUDE.md)

  • auth/ β€” crypto (keyring, session, password, api/daemon tokens, bootstrap), schemas + DDL, query_* over QueryDeps, middleware, routes, RPC action registries (admin, role-grant-offer, account, self-service-role, actor-lookup, actor-search) + standard_rpc_actions bundle, cleanup. β†’ src/lib/auth/CLAUDE.md
  • http/ β€” generic framework: RouteSpec + declarative transactions, three-layer error schema merge, JSON-RPC 2.0 envelopes + errors, origin/proxy middleware, AppSurface generation, post-commit emit_after_commit. β†’ src/lib/http/CLAUDE.md
  • actions/ β€” SAES (Symmetric Action Event System): ActionSpec types, registry-compile invariants, shared perform_action core, RPC dispatcher, REST/WS bridges, transports (HTTP, WS frontend + backend, auth guard), ActionDispatcher + serverβ†’client peer requests (peer/ping), reactive FrontendWebsocketClient, typed RPC client. β†’ src/lib/actions/CLAUDE.md
  • ui/ β€” Svelte 5 components, runes-based *_state.svelte.ts modules, *_rpc_context DI pattern, auth/admin/role-grant-offer forms, datatable, popovers, layout shell. β†’ src/lib/ui/CLAUDE.md
  • testing/ β€” test utilities exported to consumers; every module starts with import './assert_dev_env.js'. β†’ src/lib/testing/CLAUDE.md

Smaller subsystems

  • db/ β€” Db abstraction over pg + PGlite, URL-based driver auto-detection (create_db), advisory-lock migrations (run_migrations, Migration), QueryDeps pattern, assert_row for INSERT RETURNING, is_pg_unique_violation, assert_valid_sql_identifier, the *_COLUMNS projection derivation helpers (columns_sql / qualify_columns / omit_columns in sql_columns.ts, each taking an optional per-column ColumnExpr β€” iso8601_timestamp_column is the one in use, built per table by iso8601_timestamp_expr), CLI DB status utility. Also the cell content-primitive schema + queries (cell + cell_grant + cell_field + cell_item, namespace fuz_cell; dormant cell_history); its wire/RPC/authz layer lives in auth/. Plus the optional fact content-addressed byte store (fact + fact_ref + memo, namespace fuz_facts; PgFactStore over the @fuzdev/fuz_util/fact_store.ts interface) β€” size-routed embedded/disk-CAS/put_ref writes plus the bounded-memory streaming put_stream; the <shard>/<rest> disk CAS (fact_disk_storage.ts over runtime/*Deps), its URL/layout shape (file_fact_url.ts), and the put_stream error types (fact_store_errors.ts) live in db/; the read-side fetcher + write/serve plumbing live in server/. β†’ src/lib/db/CLAUDE.md
  • server/ β€” two-step assembly: create_app_backend (deps + DB + close; accepts optional migration_namespaces to splice consumer migrations after the builtin auth namespace, rejecting the reserved 'fuz_auth' name) then create_app_server (requires initialized backend). AppServerContext carries audit_sse. rpc_endpoints (HTTP RPC auto-mount) and ws_endpoints (WebSocket auto-mount β€” paired with top-level upgradeWebSocket) are the single source of truth for surface generation and live dispatch β€” each entry is auto-mounted by create_app_server, so consumers no longer call create_rpc_endpoint / register_ws_endpoint themselves. AppServer.ws_endpoints returns the path-keyed transport map for broadcast. Also env validation (validate_server_env), multi-phase SvelteKit static fallback, log_startup_summary, validate_nginx_config. Optional fact-serving plumbing (the URL/layout shape file_fact_url.ts + the streaming disk CAS now live in db/): file_fact_fetcher.ts (create_file_fact_fetcher β€” filesystem FactExternalFetcher), fact_write.ts (write_fact β€” buffering embedded-vs-disk size routing), serve_fact_route.ts (cell-scoped fact serving: create_serve_cell_fact_route_spec β†’ GET /api/cells/:cell_id/facts/:hash, the per-reference read β€” can_view_cell(caller, cell) AND cell.refs includes hash, never unioned across referrers; plus admin-only create_serve_fact_route_spec β†’ GET /api/facts/:hash. 404-masked, embedded stream or X-Accel-Redirect). The X-Accel-Redirect prefix is a make-impossible-states handle: x_accel.ts's XAccelConfig / create_x_accel_config gate it behind a fail-loud validate_facts_internal_location boot check, so X-Accel can't be enabled against a non-internal; facts location (a public one would bypass every cell-visibility check). The consumer constructs PgFactStore (db/fact_store.ts) + a fetcher and assigns deps.fact_store at its own backend assembly; create_app_backend stays facts-agnostic. BaseServerEnv carries FUZ_FACTS_DIR + FUZ_FACTS_X_ACCEL_REDIRECT_PREFIX.
  • realtime/ β€” create_sse_response, EventSpec, create_validated_broadcaster. SubscriberRegistry<T> has scope (capped by max_per_scope) + groups (uncapped) identity split; close_by_identity matches either. create_sse_auth_guard closes streams on role_grant_revoke / session_revoke / session_revoke_all / password_change (ignores outcome=failure; session_revoke scoped by session hash). AUDIT_LOG_SSE_MAX_PER_SCOPE = 10.
  • runtime/ β€” composable *Deps interfaces (EnvDeps, FsReadDeps, FsSecureReadDeps (read_secure_file β€” hardened secret read: symlink/permissive-mode/size refusal, twin of Rust fuz_sys::secure_file; Node impl in secure_file.ts), FsWriteDeps (mkdir/write_text_file/write_file/rename + fsync β€” the durability seam the fact disk CAS calls on a temp before its publishing rename; mock no-ops it; write_text_file + mkdir take {mode, exclusive} creation options), FsRemoveDeps (remove), FsStreamDeps (read_file_stream/write_file_stream β€” bounded-memory streaming for GB-scale transfer), FetchDeps, CommandDeps; bundled RuntimeDeps; StatResult carries size (byte length) + mtime_ms (epoch ms, populated by node/deno, omitted by the mock β€” read by the fact-store orphan-temp sweep)). Implementations: create_node_runtime, create_deno_runtime, create_mock_runtime (+ MockExitError). write_file_atomic in fs.ts β€” unique .{name}.tmp.{pid}.{counter} temp + exclusive create + optional mode, so a crash-leftover temp is never republished with a stale permissive mode.
  • cli/ β€” parse_command_args, create_extract_global_flags, ParseResult<T>, NO_COLOR-aware colors, run_local, confirm prompt, CliLogger, generic config loader (get_app_dir, load_config, save_config), daemon info (read_daemon_info, is_daemon_running, check_daemon_health, stop_daemon), schema-driven help (create_help, CommandMeta<T>).
  • env/ β€” load_env() Zod-schema loader + EnvValidationError, masking (format_env_display_value, MASKED_VALUE), $$VAR$$ resolution (resolve_env_vars, has_env_vars, scan_env_vars, validate_env_vars, format_missing_env_vars), dotenv parsing (parse_dotenv, load_env_file).
  • dev/ β€” consumer setup/reset helpers: setup_env_file, setup_bootstrap_token, reset_bootstrap_token, create_database, reset_database, read_env_var, generate_random_key, parse_db_name. All accept small *Deps from runtime/deps.ts.

Root-level modules

  • crypto.ts β€” generate_random_base64url(byte_length?) β€” shared randomness source
  • sensitivity.ts β€” Sensitivity = 'secret'
  • schema_meta.ts β€” SchemaFieldMeta for Zod .meta() (description + sensitivity)
  • hono_context.ts β€” Hono ContextVariableMap augmentation (includes db: Db for declarative transactions)
  • rate_limiter.ts β€” sliding-window RateLimiter, rate_limit_exceeded_response(c, retry_after) 429 helper
  • primitive_schemas.ts β€” cross-domain validators: Username, UsernameProvided, Email (split out from auth/account_schema.ts so non-auth surfaces can reach for them)
  • timestamp.ts β€” the canonical wire timestamp shape: to_iso8601_seconds(date) + the ISO8601_SECONDS regex (second-precision UTC, 20 characters). The SQL twin for row-read timestamps is iso8601_timestamp_column in db/sql_columns.ts

Shared helpers accept small *Deps from runtime/deps.ts (not Pick<GodType, ...>).

Peer Dependencies

  • hono (>=4), zod (^4), svelte (^5), @sveltejs/kit (^2)
  • @fuzdev/fuz_util (>=0.65.2)
  • @node-rs/argon2 (>=2) β€” for auth/password_argon2
  • @fuzdev/blake3_wasm (>=0.1.0) β€” for auth/session_queries, auth/bearer_auth
  • pg (>=8) or @electric-sql/pglite (>=0.4) β€” optional, for db/create_db
  • @hono/node-server (>=1), @hono/node-ws (>=1), ws (>=8) β€” optional, for the Node server adapter + WebSocket transport
  • esm-env (^1) β€” optional, for the DEV-only output-validation gate

Architecture

AppDeps Vocabulary

Three categories β€” keep them separate:

  • Capabilities (AppDeps) β€” Stateless, injectable, swappable per env: read_secure_file, delete_file, keyring, password, db, log, audit (the bound AuditEmitter β€” built by the consumer's audit_factory callback over create_audit_emitter, closes over its registered listeners + AuditLogConfig)
  • Route caps (RouteFactoryDeps) β€” Omit<AppDeps, 'db'> β€” for route factories (handlers get db via RouteContext)
  • Parameters (*Options) β€” Static startup values, per-factory: session_options, login_ip_rate_limiter, login_account_rate_limiter, token_path
  • Runtime state (inline ref) β€” Mutable values: bootstrap_status β€” NOT in deps or options

Server assembly is two explicit steps: create_app_backend (deps bundle + DB metadata + close callback) then create_app_server (requires pre-initialized AppBackend). When audit_log_sse is set, create_app_server registers audit_sse.on_audit_event via backend.deps.audit.add_listener so SSE fan-out runs alongside the consumer's callback (no shallow copy of AppDeps). Pass argon2_password_deps for production; inject stubs in tests.

The top-level create_route_specs callback receives (ctx: AppServerContext). Individual factories take narrower deps: create_account_route_specs(deps: RouteFactoryDeps, options), create_audit_log_route_specs(options?), create_db_route_specs(deps: DbRouteDeps, options) (a structural audit slice β€” ctx.deps satisfies it). Consumers destructure ctx.deps when calling them.

Middleware Ordering

create_app_server assembles middleware in order:

  1. Hono context augmentation β€” side-effect import of hono_context.ts
  2. Pending effects (*) β€” per-request array; flushed via try/finally + Promise.allSettled
  3. Logging β€” controlled by deps.log level
  4. Body size limit β€” default 1 MiB (DEFAULT_MAX_BODY_SIZE); max_body_size to override, null to disable
  5. Trusted proxy (*) β€” resolves client IP from XFF; must run before auth/rate-limiting
  6. Origin verification (/api/*)
  7. Session parsing (/api/*) β€” parses cookie, sets identity on context
  8. Request context (/api/*) β€” validates the session and sets c.var.account_id + CREDENTIAL_TYPE_KEY. Account-only β€” does not load actor or role_grants.
  9. Bearer auth (/api/*) β€” CLI clients; same account-only shape. Rejected when Origin or Referer is present.
  10. Routes β€” apply_route_specs with fuz_auth_guard_resolver (params β†’ query β†’ pre-authorization auth (401 + credential type + token scope) β†’ authorization phase β†’ post-authorization auth (403 role) β†’ input validation (400) β†’ handler). Order is 401 β†’ authz β†’ 403 β†’ 400 β†’ handler: require_auth, require_credential_types(types), and require_token_scope(capability) all read what the auth middleware set, so they fire before body parsing (no route-shape information to a caller they refuse) and before actor resolution (a wrong channel costs no DB work and learns no account state); the authorization phase then resolves the acting actor when auth.actor !== 'none' (per registry-time invariant 2, biconditionally implies the input declared acting?: ActingActor) from the acting selector β€” c.var.validated_query.acting on GETs, read off the raw body on mutations; require_role(roles) follows as the one gate needing the populated RequestContext; body validation runs last, so a 400 never describes the route to a caller the authority gates refused. Account-grain routes (auth.actor === 'none') run with RequestContext.actor: null. Same coarse-to-fine priority as the RPC dispatcher (actions/action_rpc.ts).
  11. Static serving (optional) β€” SvelteKit static fallback

Session parsing is separate from auth enforcement β€” login and bootstrap routes participate in cookie refresh without being blocked. Acting-actor resolution is separate from authentication β€” multi-actor accounts can hit account-grain routes (logout, password_change, account_verify) without picking a persona.

Route Spec System

Routes are data (RouteSpec[]). apply_route_specs registers them with auto-validation (params β†’ query β†’ pre-authorization auth β†’ authorization phase β†’ post-authorization auth β†’ input validation β†’ handler β†’ DEV-only output + error validation). Duplicate method+path throws at registration. Declarative transactions: transaction?: boolean defaults to false for GET, true for mutations. Handlers receive (c, route) where route satisfies QueryDeps; for fire-and-forget effects that must outlive the transaction (audit writes), call deps.audit.emit(route, input) β€” the bound emitter closes over the pool so the row lands even when the handler's transaction rolls back. generate_app_surface() produces a JSON-serializable attack surface. Error schemas use three-layer merge (derived + middleware + explicit β€” see ./docs/architecture.md).

Input validation runs in both DEV and production (always-on contract for callers). Output + error-schema validation runs DEV-only via esm-env β€” logs an error on mismatch, returns the response unchanged. The asymmetry is deliberate: caller-facing inputs must be validated; server-authored outputs are trusted at runtime and checked during development. See ./docs/architecture.md Β§DEV-only Output Validation.

Schema helpers live in http/schema_helpers.ts β€” import from there, not surface.ts.

Action Spec System (SAES)

One declarative ActionSpec binds to three transport surfaces (REST, JSON-RPC over HTTP, WebSocket) with uniform DEV-only output validation. Two bindings live in actions/:

  • action_rpc.ts β€” create_rpc_endpoint({path, actions, log}) produces a single JSON-RPC 2.0 endpoint (GET + POST on same path). Bind specs to handlers with rpc_action<TSpec>(spec, handler) for auto-narrowed ctx.auth.
  • action_bridge.ts β€” create_action_route_spec derives REST RouteSpec (escape hatch for SSE, files, custom paths); create_action_event_spec derives EventSpec.

ActionContext is the single handler-context shape across HTTP RPC, WS, and the REST bridge. Phase order is 401 β†’ authz β†’ 403 β†’ 429 β†’ 400 β†’ handler on every transport β€” the REST pipeline without the 429, which is the dispatcher-only per-action rate limit (ActionSpec.rate_limit). It sits ahead of validation so malformed params charge the budget too. WS authorizes per-message, so role_grant changes during a connection lifetime are picked up on the next message.

For the binding matrix, registry-time invariants, perform_action shared core, transports, codegen helpers, and reactive frontend client see src/lib/actions/CLAUDE.md. For DEV-only output validation rationale see ./docs/architecture.md Β§DEV-only Output Validation.

Action Registries

Admin + self-service surfaces are RPC-first. Each registry splits across a *_action_specs.ts (schemas + specs + registry β€” importable by typed-client codegen) and a *_actions.ts (create_*_actions(deps, options) factory with handlers).

Six factories live in auth/, surfaced via the create_standard_rpc_actions bundle (admin + role_grant_offer + account) plus three opt-in extras (self_service_role, actor_lookup, actor_search). For the full registry table, per-method specs, option routing, error reasons, audit events, and WS notification fan-out see src/lib/auth/CLAUDE.md Β§RPC action surfaces.

CreateAppServerOptions.rpc_endpoints is the single source of truth for RPC mounting β€” accepts an array or a factory (ctx: AppServerContext) => Array<RpcEndpointSpec>. create_app_server auto-mounts each via create_rpc_endpoint, so consumers no longer invoke create_rpc_endpoint themselves.

admin_rpc_adapters.ts (in ui/) exposes create_admin_rpc_adapters(api) + provide_admin_rpc_contexts(adapters) for single-call wiring of the four admin RPC contexts (admin_accounts, admin_invites, audit_log, app_settings).

Only POST /login, POST /logout, POST /password, POST /signup, POST /bootstrap, GET /verify (empty-body nginx auth_request shim β€” the typed payload lives on the account_verify RPC action), and optional GET /audit/stream (SSE) remain on REST post-migration. Consumer test suites must pass rpc_endpoints to describe_standard_integration_tests / describe_standard_admin_integration_tests / describe_audit_completeness_tests β€” they hard-fail without it.

Testing

See ./docs/testing.md for the consumer wiring guide. Skill(fuz-stack) covers shared conventions (src/test/ layout, .db.test.ts, assert from vitest, *Deps over god-type mocks). Backend tests use $lib/ imports.

Cross-process self-tests β€” fuz_app runs the standard suites against its own spine over real HTTP (not just in-process) via spawnable TS spine binaries built on the testing/cross_backend/testing_server_core.ts + Node/Deno/Bun adapters, plus the Rust testing_spine_stub. These live in the opt-in cross_backend_* vitest projects (gated behind FUZ_TEST_CROSS_BACKEND=1, excluded from a bare gro test) and the npm run benchmark:cross-impl run. See ./src/test/CLAUDE.md Β§Cross-backend self-tests and src/lib/testing/CLAUDE.md Β§"Building a TS test-server binary".

Cross-impl schema parity β€” consumers running two backend impls against a shared schema (e.g., zzz's --backend=both) use query_schema_snapshot (testing/schema_introspect.ts) + assert_schema_snapshots_equal / diff_schema_snapshots / format_schema_diffs (testing/schema_parity.ts) to gate structural drift between bootstrapped DBs. Captures tables / columns (with udt_name for int4 vs int8) / indexes / constraints / sequences / enum types (pg_enum labels in declared order); the schema_version migration tracker is always excluded (framework bookkeeping, not domain schema). That exclusion is the gate's scope, not the parity contract: the TS and Rust spines are permanent twins that must stay migration-identity-aligned β€” identical namespaces, migration names, and partitioning, so the tracker rows are byte-identical and any consumer can swap TS↔Rust over one DB without re-bootstrapping. Because the snapshot gate is provenance-agnostic (it can't see tracker drift β€” that gap let the cell/fact migration-name divergence reach the visiones cutover undetected), a separate _testing_migration_tracker gate dumps the schema_version rows and asserts the two bootstrapped spines record identical (namespace, name, sequence). Migration names are descriptive on both spines (full_auth_schema, full_cell_schema, full_cell_history_schema, full_fact_schema + named appends like role_grant_offer_and_scoped_role_grants) β€” identity, not an ordinal; the schema_version.sequence column carries order, so _vN in a name would only duplicate it. cell_history is isolated in its own fuz_cell_history namespace (spliced after fuz_cell) on both spines. Diffs are tagged-union by kind so failure messages name the specific divergence. fuz_app gates its own TS spine ↔ testing_spine_stub schema (auth + cell + cell_history + fact + the cell_visibility enum) and tracker identity via the cross_backend_parity project (npm run test:cross:parity), which also runs the action-manifest parity gate (the RPC method-set + per-method auth-shape twin of schema parity).

When working on tests, touch both directories together:

  • ./src/test/ β€” fuz_app's own suite. See ./src/test/CLAUDE.md.
  • src/lib/testing/ β€” composable helpers exported to consumers. New shared helpers belong here (every file starts with import './assert_dev_env.js'). See src/lib/testing/CLAUDE.md.

When middleware or public API gains a new context variable, header, or field, update both the shared echo/mocks in src/lib/testing/middleware.ts and the assertions in src/test/auth/*.test.ts.

Consumer Patterns

  • Full-stack web app β€” Auth, admin routes, route specs, SSE, db routes, CLI, env, static, create_db, UI components
  • Local daemon (PGlite) β€” Full auth stack + admin routes, bootstrap with on_bootstrap, CLI. See ./docs/local-daemon.md
  • Action-oriented app β€” Action specs, CLI (runtime, util, config, daemon, help)

Committing

git add and git commit are denied by .claude/settings.local.json in this repo β€” make the edits and stop, the user commits.