Skip to main content

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

ProfileDefault compositionSupported choices
localuser-owned SQLite; single-use owner bootstrap; real default tenant; local assets and secrets; embedded jobs; loopback binding; file-snapshot backupjobs may run inline; TLS may be enabled while the bind remains loopback
self-hostedoperator PostgreSQL; OIDC; single tenant; S3-compatible assets; environment secrets; external workers; public TLS; operator backupmagic link auth; explicit multi-tenant mode with required context; local or S3-compatible assets; environment, local-file, or external secrets; application isolation or RLS
cloudmanaged PostgreSQL; hosted identity; required multi-tenant context with RLS; managed storage and secrets; scalable workers; public TLS; managed backupS3-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

ProfileSupported application-side choices
self-hostedOIDC 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
cloudHosted 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.