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.
Anatomy
Section titled “Anatomy”// @skip-typecheckimport { 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.
Identity
Section titled “Identity”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.
Server binding
Section titled “Server binding”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.
Composition
Section titled “Composition”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.
Database
Section titled “Database”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.
Security
Section titled “Security”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.
Plugins and packages
Section titled “Plugins and packages”// @skip-typecheckimport { 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.
Event bus
Section titled “Event bus”// @skip-typecheckimport { 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.
Secrets
Section titled “Secrets”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.
Realtime
Section titled “Realtime”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.
Multi-tenancy
Section titled “Multi-tenancy”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.
Runtime
Section titled “Runtime”// @skip-typecheckimport { 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.
Versioning
Section titled “Versioning”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.
What’s next
Section titled “What’s next”- Routes and Handlers — write a route
- Packages — organize features
- Plugin Interface — write a
SlingshotPluginfor auth, persistence, queues, or other framework-level infrastructure - Lower-level Seams — overrides and manual wiring inside the package model
- Entities — declare a data model