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.
Standard wiring checks the complete resolved entity and operation contract before opening a
backend connection. If the selected backend cannot honor required semantics, startup throws
UnsupportedEntityBackendError with every missing capability and the configuration that
required it. This prevents a backend switch from silently weakening behavior.
See the generated entity backend support matrix for the
exact semantic claims and unsupported reasons for every standard store.
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.
Inspect backend capabilities
Section titled “Inspect backend capabilities”Use the same immutable profiles that startup validation uses:
import { ENTITY_BACKEND_PROFILES, getEntityBackendProfile } from '@lastshotlabs/slingshot-entity';
const redis = getEntityBackendProfile('redis');redis.capabilities['constraint.unique'].status; // "unsupported"ENTITY_BACKEND_PROFILES.postgres.capabilities['transaction.rollback'].status; // "supported"Current important boundaries:
- Redis standard wiring rejects entities with secondary or compound unique constraints.
- Memory, MongoDB, and Redis reject composite
op.transactionbecause rollback is unavailable. - A standard
op.customrequires a factory for the active backend. - Primary-key create is insert-only on every standard adapter; MongoDB and Redis do not overwrite existing records.
- Framework transaction scopes contain only opaque identity. They never expose a PostgreSQL client, SQLite handle, Drizzle transaction, raw query function, or MongoDB session.
- Resolve scoped adapters through package or hook
entities.get(..., { scope }); adapters resolved beforetransactions.run()remain unscoped.
Use manual adapter wiring when your application intentionally provides semantics outside the standard factory contract.
Source-backed startup examples
Section titled “Source-backed startup examples”This PostgreSQL entity configuration is accepted because PostgreSQL claims and proves configured uniqueness:
import { assertEntityBackendRequirements, defineEntity, field,} from '@lastshotlabs/slingshot-entity';
const Account = defineEntity('Account', { fields: { id: field.string({ primary: true }), email: field.string(), }, uniques: [{ fields: ['email'] }],});
assertEntityBackendRequirements('postgres', Account);The same configuration is rejected for standard Redis wiring before the Redis client is opened:
import { UnsupportedEntityBackendError, assertEntityBackendRequirements, defineEntity, field,} from '@lastshotlabs/slingshot-entity';
const Account = defineEntity('Account', { fields: { id: field.string({ primary: true }), email: field.string(), }, uniques: [{ fields: ['email'] }],});
try { assertEntityBackendRequirements('redis', Account);} catch (error) { if (error instanceof UnsupportedEntityBackendError) { console.error(error.code); // "SLINGSHOT_ENTITY_BACKEND_UNSUPPORTED" console.error(error.missing[0]?.capability); // "constraint.unique" }}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.
Concurrency-enabled entities are supported by memory, SQLite, PostgreSQL, and MongoDB adapters. Their update and delete implementations compare the expected version and mutate atomically; SQLite and PostgreSQL preserve those semantics inside transaction scopes. Redis rejects the configuration before connecting because it cannot yet provide the required atomic guarantee. See Optimistic Concurrency and the generated backend support matrix.
- defineEntity — the entity model contract
- Generated Routes, Overrides, and Extra Routes — customize generated routes