Application runtime profiles
An s-m-r-t application declares one infrastructure profile and keeps the same domain objects, workflows, generated interfaces, and approval policy when it moves between a laptop and a deployment.
import { defineConfig } from '@happyvertical/smrt-config';
export default defineConfig({
runtime: { profile: 'local' },
});
At startup, call resolveConfiguredApplicationRuntime(). It composes the loaded
file with highest-priority setConfig() overrides, then returns a deterministic,
immutable, machine-readable snapshot for startup, doctor, tests, and agent
inspection. It reads no environment variables and contains no connection
strings, paths, credentials, or secret values. Use
resolveApplicationRuntime(config) only when resolving an explicit value, such
as a test fixture; getConfig() retains its legacy loaded-file-only semantics.
Safe presets
| Profile | Default composition | Supported choices |
|---|---|---|
local | user-owned SQLite; single-use owner bootstrap; real default tenant; local assets and secrets; embedded jobs; loopback binding; file-snapshot backup | jobs may run inline; TLS may be enabled while the bind remains loopback |
self-hosted | operator PostgreSQL; OIDC; single tenant; S3-compatible assets; environment secrets; external workers; public TLS; operator backup | magic link auth; explicit multi-tenant mode with required context; local or S3-compatible assets; environment, local-file, or external secrets; application isolation or RLS |
cloud | managed PostgreSQL; hosted identity; required multi-tenant context with RLS; managed storage and secrets; scalable workers; public TLS; managed backup | S3-compatible storage, external secret provider, or application-enforced isolation |
All three presets advertise logical export and import. Provider-specific settings such as database URLs, asset roots, OIDC credentials, and secret manager identifiers remain in the owning provider's configuration. The runtime snapshot intentionally reports only safe selectors and derived capabilities.
Explicit overrides
Profiles are presets, not environment-name conditionals. Apply supported provider changes in one place:
export default defineConfig({
runtime: {
profile: 'self-hosted',
providers: {
authentication: { provider: 'magic-link' },
tenancy: { mode: 'multi-tenant', context: 'required' },
assets: { provider: 'local-files' },
},
},
});
The resolver reports effective changes under diagnostics.overrides. Unknown
fields and incompatible combinations fail before application startup with a
path, reason, and recovery action. For example, local owner bootstrap cannot be
combined with a public bind, and cloud mode cannot disable required tenant
context or TLS.
When a runtime override selects a different profile, file-level provider
overrides are discarded before the new preset resolves. This prevents a local
choice such as inline jobs or local files from leaking into a cloud composition.
The same reset applies when successive setConfig() calls explicitly change
profile. When the profile stays the same, nested provider overrides deep-merge
normally.
Cross-profile invariants
Provider selection cannot change these application behaviors:
- domain models and workflows;
- generated REST and CLI surfaces;
- generated MCP and WebMCP definitions;
- action-effect metadata and human approval policy;
- real authorization records;
- the job invocation API;
- logical export/import portability.
The resolved snapshot includes these invariants so integration tests can compare profiles directly. Implementing packages consume the contract; templates should never copy profile conditionals into application code.
Running the local profile
@happyvertical/smrt-app-runtime implements the private local composition. The
application supplies its normal idempotent migration hook; the runtime prepares
the user-owned filesystem, securely acquires and tunes file-backed SQLite,
creates local application-secret material, and issues the first short-lived
onboarding token.
import {
initializeLocalApplicationRuntime,
} from '@happyvertical/smrt-app-runtime';
const { runtime, bootstrap, diagnostics } =
await initializeLocalApplicationRuntime({
appId: 'my-app',
sourceRoot: process.cwd(),
prepareDatabase: runApplicationMigrations,
});
The default root is the operating system's per-user application-data directory,
never the source checkout, one of its ancestors, the user home itself, or the
filesystem root. The application root is a dedicated directory; when an
explicit root already exists, it must already be
owned by the current user with mode 0700, and a failed proof leaves its mode
and contents untouched. Empty roots receive an app-specific atomic ownership
marker; populated roots require that marker. A pending marker safely resumes a
crash between root claim and database acquisition, while failed SQL custody
validation removes only a claim and directories created by the failing attempt;
an inherited pending claim and database remain available for retry. The SQL
boundary proves ancestor ownership/modes and macOS ACL safety before database,
asset, or secret artifacts are created. Directories are mode 0700; the database and
generated application-secret file are mode 0600. SQLite enables foreign keys,
WAL, full synchronous durability, and a busy timeout. The local server binds to
127.0.0.1 by default, and owner bootstrap refuses a non-loopback bind.
Concurrent initializers are serialized across processes by an exclusive
transaction in a dedicated SQLite lock database under a private per-user,
root-keyed custody directory. The released SQL trusted-parent boundary validates
the lock directory, ancestor chain, leaf, and macOS ACLs before opening it.
SQLite elects one owner atomically without deleting a lock pathname; process or
worker death releases the kernel lock without PID/time leases. A read-only
storage-path and marker preflight rejects obvious invalid configurations before
creating a registry entry, and the complete validation runs again under the
lease before application-root mutation. The lease is held through secret
publication, SQLite tuning, application migrations, and bootstrap construction;
contenders wait up to two minutes for completion.
Initialization walks every existing storage-path component without following
symbolic links, verifies canonical source-tree separation, and performs chmod
through validated file descriptors. A platform without the required no-follow
file and directory semantics is refused rather than initialized unsafely. After
establishing its mode-0700 data root, the runtime acquires SQLite through the
@happyvertical/sql node:sqlite trusted-parent custody boundary. That boundary
rejects unsafe ownership, write permissions, static links, macOS ACLs, and
unsupported platforms or Node runtimes before opening the database.
The application secret is written and synced to a private temporary file, then
published with a no-overwrite atomic hard link. Concurrent startup reuses the
single complete winner; malformed existing values fail closed and interrupted
temporary files are cleaned only after a valid final secret exists.
It protects the custodied directory from other OS principals, not hostile code
already running as the same user; that stronger boundary requires OS sandboxing
and a descriptor-relative SQLite VFS.
Only an HMAC of the onboarding token is stored. The plaintext is returned once,
expires within fifteen minutes, and is consumed in the same serialized database
transaction that creates the real global Person, User, default Tenant,
owner Role / Membership, and server-side Session. Repeated setup or startup
does not duplicate those records. Tenancy can remain hidden in the local UI, but
the durable default tenant is preserved for later logical migration.
Authenticated session TTLs are whole seconds with a minimum of one second;
invalid TTL configuration is rejected before any filesystem mutation.
Background work and application-defined paid capabilities are disabled by
default. An explicit background opt-in exposes the regular embedded
TaskRunner, preserving the same persisted enqueue and execution contract used
by deployed workers. Diagnostics report these choices and bootstrap state but
never read or emit application secrets, token plaintext, or token hashes.
Running self-hosted and cloud profiles
initializeDeployedApplicationRuntime() composes the application-side seams
for public deployments. Provider-specific URLs, credentials, clients, and
vendor configuration stay in the provider bindings; the runtime receives only
their selector, readiness boundary, and a PostgreSQL connection factory.
import {
initializeDeployedApplicationRuntime,
} from '@happyvertical/smrt-app-runtime';
import { getDatabase } from '@happyvertical/sql';
const runtime = await initializeDeployedApplicationRuntime({
profile: 'self-hosted',
providers: {
authentication: { provider: 'magic-link' },
tenancy: { mode: 'multi-tenant', context: 'required' },
assets: { provider: 'local-files' },
secrets: { provider: 'external' },
},
database: {
engine: 'postgres',
connect: () => getDatabase(postgresPrivateConfig),
close: async (db) => {
await db.close?.();
},
},
authentication: {
provider: 'magic-link',
readiness: () => magicLinkProvider.assertReady(),
},
assets: {
provider: 'local-files',
readiness: () => localAssetProvider.assertReady(),
},
secrets: {
provider: 'external',
readiness: () => externalSecrets.assertReady(),
},
prepareDatabase: runApplicationMigrations,
});
The initializer validates the profile, exact adapter identities, and a
provider-owned database cleanup boundary before opening PostgreSQL. It then
checks public authentication, assets, and secrets,
probes the database, and runs the explicit idempotent migration hook. Any
failure closes the acquired database and rejects startup. Stable failures,
diagnostics, health, and readiness never include the provider's private error
text or returned secret values.
Provider-specific database readiness is additive: the runtime always performs
a PostgreSQL-specific server-version probe before startup or readiness can
succeed.
If that startup cleanup fails, the redacted
DeployedRuntimeCleanupError.retryCleanup() remains the explicit owner until
the provider closes successfully.
The web, task-worker, and schedule-worker processes use the same initialization
contract. Task and schedule processes call createTaskWorker() or
createScheduleWorker() and then start the returned ordinary s-m-r-t runner.
Application code still enqueues through bg() or
background(...).enqueue(); profile selection does not alter that API.
Runtime shutdown drains in-flight readiness/session/worker initialization and
serialized runner start/stop operations, stops every runner returned by that
runtime, and then closes PostgreSQL. A caller may stop a runner earlier, but
must not restart it after runtime shutdown has begun; the lifecycle-gated
start() method rejects at that point.
An awaited close() request from inside a tracked provider or worker operation
acknowledges shutdown so the operation can unwind; external callers still await
complete worker and database cleanup.
Health reports whether this runtime instance is live. Readiness rechecks PostgreSQL, authentication, assets, and secrets. It deliberately does not claim that another process or worker replica is alive; worker-fleet monitoring belongs to the operator or managed platform. Diagnostics report explicit provider selectors, tenant context/isolation, separate worker roles, and horizontal cloud topology without credentials.
Supported deployed choices
| Profile | Supported application-side choices |
|---|---|
self-hosted | OIDC or magic link; explicit single tenant with defaulted context or multi-tenant with required context; application isolation or RLS; local or S3-compatible assets; environment, local-file, or external secrets; separate task and schedule workers |
cloud | Hosted identity; required multi-tenant context; application isolation or RLS; managed or S3-compatible object storage; managed or external secrets; horizontally scalable workers |
The following combinations are refused before application startup: SQLite in a deployed profile, owner bootstrap or loopback exposure for a public deployment, missing/mismatched provider bindings, non-TLS public configuration, embedded deployed workers, cloud single/default tenancy, cloud defaulted tenant context, and operator-owned providers in the managed profile.
The composition seam does not provision PostgreSQL, TLS, buckets, identity
providers, secret managers, workers, billing, or a hosted control plane. The
operator applies application migrations and, when database-rls is selected,
the documented s-m-r-t PostgreSQL policies. The selector validates intent; it
does not mutate database roles or privileges during web startup.