Skip to content

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.

When you do not configure a database, entities use in-memory storage:

app.config.ts
// @skip-typecheck
import { 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.

One config change:

app.config.ts
// @skip-typecheck
export 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.

app.config.ts
// @skip-typecheck
export 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.

app.config.ts
// @skip-typecheck
export default defineApp({
port: 3000,
db: { default: { adapter: 'mongo', url: process.env.MONGO_URL! } },
packages: [definePackage({ name: 'app', entities: [entity({ config: Task })] })],
});

Best for cache-shaped entities like sessions, rate limits, or ephemeral state:

app.config.ts
// @skip-typecheck
export default defineApp({
port: 3000,
db: { default: { adapter: 'redis', url: process.env.REDIS_URL! } },
packages: [definePackage({ name: 'app', entities: [entity({ config: Task })] })],
});

Different entities can use different backends:

app.config.ts
// @skip-typecheck
export 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
],
}),
],
});

For advanced control, use createEntityFactories to get a set of backend-specific adapter constructors:

src/entities/post-factories.ts
// @skip-typecheck
import { 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-typecheck
postFactories.memory(); // in-memory adapter
postFactories.sqlite(infra); // SQLite adapter
postFactories.postgres(infra); // Postgres adapter
postFactories.mongo(infra); // MongoDB adapter
postFactories.redis(infra); // Redis adapter

Wire it into the entity module with wiring: { mode: 'factories' }:

src/packages/blog.ts
// @skip-typecheck
entity({
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.

When a package owns several related entities and you want one combined adapter:

// @skip-typecheck
import { 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.

For complete control over adapter construction — useful when integrating with an existing database or custom ORM:

// @skip-typecheck
entity({
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).

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.