Storage and Adapter Wiring
The entity system separates your data model from your storage backend. Define the entity once, then point it at memory for development, SQLite for prototyping, or Postgres for production — the entity code stays the same.
Start with in-memory (the default)
Section titled “Start with in-memory (the default)”When you do not configure a database, entities use in-memory storage:
// @skip-typecheckimport { defineApp, definePackage, entity } from '@lastshotlabs/slingshot';import { Task } from './src/entities/task';
export default defineApp({ port: 3000, packages: [definePackage({ name: 'app', entities: [entity({ config: Task })] })],});Data lives in memory and resets on restart. This is perfect for development and tests.
Switch to SQLite
Section titled “Switch to SQLite”One config change:
// @skip-typecheckexport default defineApp({ port: 3000, db: { default: { adapter: 'sqlite', url: './data/app.db' } }, packages: [definePackage({ name: 'app', entities: [entity({ config: Task })] })],});The entity definition and all your routes stay exactly the same. Data now persists to disk.
Switch to Postgres
Section titled “Switch to Postgres”// @skip-typecheckexport default defineApp({ port: 3000, db: { default: { adapter: 'postgres', url: process.env.DATABASE_URL! } }, packages: [definePackage({ name: 'app', entities: [entity({ config: Task })] })],});Same entity code. Same routes. Different backend.
Switch to MongoDB
Section titled “Switch to MongoDB”// @skip-typecheckexport default defineApp({ port: 3000, db: { default: { adapter: 'mongo', url: process.env.MONGO_URL! } }, packages: [definePackage({ name: 'app', entities: [entity({ config: Task })] })],});Switch to Redis
Section titled “Switch to Redis”Best for cache-shaped entities like sessions, rate limits, or ephemeral state:
// @skip-typecheckexport default defineApp({ port: 3000, db: { default: { adapter: 'redis', url: process.env.REDIS_URL! } }, packages: [definePackage({ name: 'app', entities: [entity({ config: Task })] })],});Mix backends in one app
Section titled “Mix backends in one app”Different entities can use different backends:
// @skip-typecheckexport default defineApp({ port: 3000, db: { default: { adapter: 'postgres', url: process.env.DATABASE_URL! }, cache: { adapter: 'redis', url: process.env.REDIS_URL! }, }, packages: [ definePackage({ name: 'app', entities: [ entity({ config: Post }), // uses default (Postgres) entity({ config: Session, adapter: 'cache' }), // uses Redis ], }), ],});Entity factories
Section titled “Entity factories”For advanced control, use createEntityFactories to get a set of backend-specific adapter
constructors:
// @skip-typecheckimport { createEntityFactories } from '@lastshotlabs/slingshot';import { Post } from './post';import { PostOps } from './post-operations';
export const postFactories = createEntityFactories(Post, PostOps);This gives you typed factory methods:
// @skip-typecheckpostFactories.memory(); // in-memory adapterpostFactories.sqlite(infra); // SQLite adapterpostFactories.postgres(infra); // Postgres adapterpostFactories.mongo(infra); // MongoDB adapterpostFactories.redis(infra); // Redis adapterWire it into the entity module with wiring: { mode: 'factories' }:
// @skip-typecheckentity({ config: Post, operations: PostOps, wiring: { mode: 'factories', factories: postFactories, },});This is useful when you need to test against a specific backend or access the adapter directly in test code.
Composite factories
Section titled “Composite factories”When a package owns several related entities and you want one combined adapter:
// @skip-typecheckimport { createCompositeFactories } from '@lastshotlabs/slingshot';import { LineItem } from './line-item';import { LineItemOps } from './line-item-operations';import { Order } from './order';import { OrderOps } from './order-operations';
const orderFactories = createCompositeFactories({ orders: { config: Order, operations: OrderOps }, lineItems: { config: LineItem, operations: LineItemOps },});The composite adapter gives you orderFactories.memory().orders and
orderFactories.memory().lineItems from a single factory surface.
Manual adapter wiring
Section titled “Manual adapter wiring”For complete control over adapter construction — useful when integrating with an existing database or custom ORM:
// @skip-typecheckentity({ config: Post, wiring: { mode: 'manual', buildAdapter: (storeType, infra) => { // Build your own adapter implementation return createCustomPostAdapter(infra); }, },});The adapter must implement the standard entity adapter interface (get, list, create, update, delete, plus any operation methods).
The key insight
Section titled “The key insight”Backend swapping works because the entity definition is a contract, not an implementation. Fields, indexes, operations, and route policy describe what — the adapter handles how.
That separation means:
- Your dev environment uses memory (fast, no setup)
- Your CI uses SQLite (persistent, no Docker)
- Your staging uses Postgres (production-like)
- Your production uses Postgres + Redis (performance)
All running the same entity code.
- defineEntity — the entity model contract
- Generated Routes, Overrides, and Extra Routes — customize generated routes