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.
smrt app and the SvelteKit runtime resolve through
resolveEffectiveApplicationRuntime(loadedConfig): no runtime block selects
local, and a present value that is not a block (null, false, 0, '')
fails closed.
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 or password 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, root-keyed
custody directory beside the data root (<parent>/.smrt-lease-<key>), so it
also works in user-namespace sandboxes where /tmp reads as owned by the
overflow uid. The legacy per-user /tmp/.smrt-<uid> lease is taken afterwards
so older releases stay excluded; when /tmp's owner is unmapped it is taken
lock-only (never written, private current-user lock directories and file), and
it is required when the parent cannot hold a lease. In a uid-remapping
namespace both the in-namespace <uid> and its /proc/self/uid_map host
mapping are locked, in that order, so older releases inside the namespace and
on the host both contend; the in-namespace candidate is skipped only when its
root is an existing real directory owned by another user (no one in the
namespace can hold it), and an unusable map fails closed. The released SQL trusted-parent boundary validates
each 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. The
ownerTenant option chooses that tenant's slug (default default) and name;
a claim never writes over an existing tenant with the slug: it fails closed with
owner_tenant_conflict unless ownerTenant.adoptExisting is set, and then
adopts only an active root tenant that has no memberships, keeping its id.
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 first import the application's compiled
object registration, .smrt/runtime/register.js (produced by the SvelteKit
build), then call createTaskWorker() or createScheduleWorker() and 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.
SvelteKit server hooks
SvelteKit applications consume the profiles through
@happyvertical/smrt-app-runtime/sveltekit instead of copying profile
conditionals, tenant selection, and diagnostics glue into each app:
// src/hooks.server.ts
import { createSmrtSvelteKitRuntime } from '@happyvertical/smrt-app-runtime/sveltekit';
export const runtime = createSmrtSvelteKitRuntime();
export const { handle, init } = runtime;
init runs the profile's fail-closed startup (local runtime, or deployed
bindings, which additionally require explicit providerReadiness probes).
handle waits for startup, then resolves a URL tenant candidate as selection
only, loads the signed session, and enters and publishes its tenant context
only for an active direct or legitimately inherited membership; any other
session is treated as unauthenticated. Profiles that select database-rls
isolation always run the request inside the PostgreSQL RLS transaction, even on
session skip paths (as an anonymous principal), and runtime.classOptions()
then hands application collections that transaction-bound database. The
same entry provides mountable health, authorized diagnostics, the local
owner-setup load/actions (loopback peer and loopback host re-checked per
request), and a session layout summary. See the package README for options.