Skip to content

App Config

defineApp({...}) is the single declarative surface for a Slingshot app. This page walks the config object section by section so you know every knob that exists, what its default is, and when you’d touch it.

app.config.ts
// @skip-typecheck
import { defineApp } from '@lastshotlabs/slingshot';
export default defineApp({
// app identity + server binding
meta: { name: 'my-app', version: '1.0.0' },
port: 3000,
// composition
packages: [], // feature modules
plugins: [], // cross-cutting integrations
routesDir: './src/routes',
// infrastructure
db: {}, // postgres / sqlite / mongo / redis
secrets: {}, // env / file / ssm / custom
eventBus: undefined, // in-process by default
// request shape
security: {}, // signing, cors, csrf, rate limit
middleware: [], // extra Hono middleware
validation: {}, // error formatting
upload: {}, // file uploads
versioning: ['v1', 'v2'],
// realtime
ws: { endpoints: {} },
sse: { endpoints: {} },
// ops
jobs: {}, // orchestration / job queues
logging: {}, // structured logs
metrics: {}, // prometheus
observability: {}, // tracing
// multi-tenancy
tenancy: {}, // tenant resolution
// runtime selection (bun / node / edge)
runtime: undefined,
});

Every field is optional. An empty defineApp({}) is valid. The framework supplies sensible defaults and only complains about the fields you actually need.

defineApp({
meta: { name: 'my-app', version: '1.0.0' },
});

meta.name and meta.version flow into OpenAPI’s info block, OpenTelemetry service attributes, and structured log entries. Set them in production. They default to "Bun Core API" and "1.0.0" if omitted.

defineApp({
port: 3000,
hostname: '0.0.0.0', // optional; '0.0.0.0' is the default
unix: '/var/run/app.sock', // mutually exclusive with port/hostname/tls
tls: {
/* Bun TLS options */
},
maxRequestBodySize: 50 * 1024 * 1024,
});

The PORT env var overrides port — the standard Twelve-Factor pattern. unix is for sidecar / proxy deployments where binding a port is undesirable.

The composition surface is three layers, and you almost always use packages:

defineApp({
packages: [blogPackage, analyticsPackage], // typed feature modules
plugins: [createAuthPlugin({...})], // cross-cutting concerns
routesDir: './src/routes', // file-system routing
});

packages is the canonical surface — see Packages. plugins is for integrations like auth, organizations, webhooks, search. routesDir is for top-level routes that don’t belong to any package, or for quick experiments.

The boot order is: plugins → packages → routesDir. Plugin middleware runs before package middleware, package routes mount before discovered routes.

defineApp({
db: {
postgres: process.env.DATABASE_URL,
redis: process.env.REDIS_URL,
mongo: false,
sqlite: false,
},
});

Each connection is opt-in. The framework only opens connections you reference. redis: true or redis: '<url>' enables Redis (used by sessions, rate limits, BullMQ when configured). mongo: 'single' opens one connection used for both auth state and app data; 'separate' opens two distinct ones.

For SQLite, pass a file path or ':memory:'. The framework handles WAL mode and connection pooling per the runtime adapter.

defineApp({
security: {
signing: { secret: process.env.JWT_SECRET! },
cors: { origin: ['https://myapp.com'], credentials: true },
csrf: { enabled: true },
rateLimit: { windowMs: 60_000, max: 100 },
trustProxy: 1,
captcha: { provider: 'turnstile', siteKey: '...', secretKey: '...' },
headers: {
contentSecurityPolicy: "default-src 'self'",
},
botProtection: {
enabled: true,
blocklistCidrs: [
/* ... */
],
},
},
});

signing.secret is the JWT signing key — required when auth is in play. The secrets provider can populate it automatically from JWT_SECRET, in which case you can omit the inline value.

Defaults are deliberately strict: CORS denies all origins, CSRF disabled (apps that don’t use cookies don’t need it), rate limit at 100 req/60s, headers locked down. Loosen what you need.

// @skip-typecheck
import { createAuthPlugin } from '@lastshotlabs/slingshot-auth';
import { createPermissionsPackage } from '@lastshotlabs/slingshot-permissions';
defineApp({
plugins: [createAuthPlugin({ auth: { roles: ['user'], defaultRole: 'user' } })],
packages: [createPermissionsPackage({ adapter: 'postgres' })],
});

Plugin-tier factories go in plugins: and feature packages authored with definePackage go in packages:. Both are typed objects with name + lifecycle hooks. The framework topo-sorts them by declared dependencies and runs setupMiddleware, setupRoutes, and setupPost in that order. See Plugin Interface.

// @skip-typecheck
import { createBullMQAdapter } from '@lastshotlabs/slingshot-bullmq';
defineApp({
eventBus: createBullMQAdapter({
connection: { host: process.env.REDIS_HOST!, port: 6379 },
prefix: 'myapp:events',
}),
});

Default is in-process. Pass a SlingshotEventBus instance — createBullMQAdapter, createKafkaAdapter, or your own — for cross-instance event delivery.

defineApp({
secrets: {
provider: 'env',
prefix: 'APP_',
},
});

Providers: 'env' (process.env, optional prefix filter), 'file' (.env file), 'ssm' (AWS SSM Parameter Store). Pass a SecretRepository instance for custom backends like Vault. The provider resolves at boot, before any plugin runs. Missing secrets fail loud.

defineApp({
ws: {
endpoints: {
'/ws': {
auth: { mode: 'required' },
rateLimit: { messagesPerSecond: 10 },
recovery: { windowMs: 120_000 },
},
},
},
sse: {
endpoints: {
'/__sse/notifications': {
events: ['notification:new'],
},
},
},
});

WebSocket and SSE are first-class. See WebSockets and Server-Sent Events.

defineApp({
tenancy: {
resolveTenant: c => c.req.header('x-tenant-id') ?? null,
exemptPaths: ['/health', '/auth/*'],
},
});

When set, every request runs through tenant resolution before routes. Tenant ID lands on the request context (getRequestTenantId(c)) and on actor scope. Exempt paths skip resolution.

// @skip-typecheck
import { nodeRuntime } from '@lastshotlabs/slingshot-runtime-node';
defineApp({
runtime: nodeRuntime(),
});

Bun is auto-detected — omit runtime on Bun. Pass nodeRuntime() when running on Node or edgeRuntime() for Cloudflare Workers / Deno Deploy.

defineApp({
versioning: ['v1', 'v2'],
routesDir: './src/routes',
});

When set, routes are discovered from routes/v1/ and routes/v2/. Each version gets its own OpenAPI spec at /{version}/openapi.json and Scalar docs at /{version}/docs. The root /docs becomes a version selector.