Skip to content

Introduction

effect-dynamodb is an Effect TS ORM for DynamoDB providing Schema-driven entity modeling, single-table design as a first-class pattern, automatic key composition, type-safe pipeable queries, and DynamoDB client as an Effect Service.

Four declarations drive everything:

Schema.Class → Entity → Table → DynamoClient.make()
(domain) (model + (physical) (typed gateway)
keys +
indexes)
  1. Model — A standard Schema.Class defining your domain fields. No DynamoDB concepts.
  2. Entity — Binds model to a primary key + GSI indexes with system fields (timestamps, versioning, soft delete). Indexes with a collection property enable cross-entity queries.
  3. Table — Physical DynamoDB table: schema namespace, registered entities.
  4. DynamoClient.make() — Typed execution gateway. Auto-discovers collections from entity indexes, returns namespaced client with R = never.

Seven types are derived automatically from entity declarations — no manual type maintenance:

TypeDescription
Entity.Model<E>Pure domain object
Entity.Record<E>Domain + system metadata (version, timestamps)
Entity.Input<E>Creation input (no system fields)
Entity.Update<E>Mutable fields only (keys and immutable excluded)
Entity.Key<E>Primary key attributes
Entity.Item<E>Full unmarshalled DynamoDB item
Entity.Marshalled<E>DynamoDB AttributeValue format
  • Domain models are portable. A User schema works with DynamoDB, SQL, or API responses.
  • Entities define all their indexes. Primary key + GSI access patterns on the entity. Cross-entity collections auto-discovered from matching collection properties.
  • Convention over configuration. Declare which attributes compose keys, not how. The system handles format, delimiters, and serialization.
  • Fluent query builder. BoundQuery chains naturally — .filter().limit().collect() — with type-safe combinators.
  • Type safety from declarations. All types derived automatically. Change the Entity, types update everywhere.
import { Schema, Effect, Console, Layer } from "effect"
import { DynamoSchema, Table, Entity, DynamoClient } from "effect-dynamodb"
// 1. Model
class Task extends Schema.Class<Task>("Task")({
taskId: Schema.String,
projectId: Schema.String,
title: Schema.NonEmptyString,
status: Schema.Literals(["todo", "active", "done"]),
}) {}
// 2. Entity — primary key + GSI indexes
const Tasks = Entity.make({
model: Task,
entityType: "Task",
primaryKey: {
pk: { field: "pk", composite: ["taskId"] },
sk: { field: "sk", composite: [] },
},
indexes: {
byProject: {
name: "gsi1",
pk: { field: "gsi1pk", composite: ["projectId"] },
sk: { field: "gsi1sk", composite: ["status"] },
},
},
timestamps: true,
versioned: true,
})
// 3. Table — register entities
const AppSchema = DynamoSchema.make({ name: "myapp", version: 1 })
const mainTable = Table.make({ schema: AppSchema, entities: { Tasks } })
// 4. Use
const program = Effect.gen(function* () {
const db = yield* DynamoClient.make({
entities: { Tasks },
tables: { mainTable },
})
// Create table if it doesn't exist
yield* db.tables.mainTable.describe().pipe(
Effect.catchTag("ResourceNotFoundError", () =>
db.tables.mainTable.create()
),
)
yield* db.entities.Tasks.put({
taskId: "t-1", projectId: "p-1",
title: "Ship it", status: "active",
})
const active = yield* db.entities.Tasks
.byProject({ projectId: "p-1", status: "active" })
.collect()
yield* Console.log(active)
})
Effect.runPromise(program.pipe(
Effect.provide(
Layer.mergeAll(
DynamoClient.layer({
region: "us-east-1",
endpoint: "http://localhost:8000",
credentials: { accessKeyId: "local", secretAccessKey: "local" },
}),
// Physical DynamoDB table name — required runtime configuration
mainTable.layer({ name: "my-app-table" }),
)
)
))