Putnami
DocsGitHub

Licensed under FSL-1.1-MIT

Getting Started
Concepts
How To
Build A Web App
Build An Api Service
Share Code Between Projects
Configure Your App
Add Persistence
Add Authentication
Add Background Jobs
Develop With Ai
Structure Business Logic With Di
Upgrade Putnami
Principles
Tooling & Workspace
Workspace
Cli
Jobs & Caching
Extensions
Templates
Error Handling
Frameworks
Typescript
ExtensionOverviewWebReact RoutingForms And ActionsStatic FilesApiErrors And ResponsesConfigurationLoggingHttp And MiddlewareDependency InjectionPlugins And LifecycleSessionsAuthPersistenceDocumentEventsStorageCachingWebsocketsTestingHealth ChecksTelemetryProto GrpcSmart ClientSchema
Go
ExtensionOverviewHttpDependency InjectionPlugins And LifecycleConfigurationSecurityPersistenceErrorsEventsStorageCachingLoggingTelemetryGrpcService ClientsValidationOpenapiTesting
Python
Extension
Platform
Ci
  1. DocsSeparator
  2. FrameworksSeparator
  3. TypescriptSeparator
  4. Document

Document Storage

Use @putnami/document when your data is document-shaped and you want typed repositories, schema validation, cursor pagination, and pluggable document backends.

This sits next to:

  • @putnami/database for relational PostgreSQL data
  • @putnami/storage for blobs and file uploads

Installation

bunx putnami deps add @putnami/document

If you use Firestore, install its optional peer dependency too:

bunx putnami deps add @google-cloud/firestore

Enable the plugin

import { application } from '@putnami/application';
import { document } from '@putnami/document';

export const app = () => application().use(document());

The plugin closes cached adapters on shutdown.

Define collections

import { Collection, DateIso, DocumentId, Email, Field, Optional } from '@putnami/document';

export const UsersCollection = Collection(
  'users',
  {
    id: DocumentId(String),
    email: Field(Email),
    name: Field(String),
    nickname: Field(Optional(String)),
    createdAt: Field(DateIso),
  },
  {
    indexes: [{ fields: ['email'] }],
  },
);

Collection() declares:

  • the collection name
  • the document schema
  • optional named-store and index metadata

DocumentId() marks one or more fields as the document id. The public type system supports composite ids. The in-memory adapter supports them today; the Firestore adapter currently requires a single scalar id.

Create a repository

import { Repository } from '@putnami/document';
import { UsersCollection } from './users.collection';

export class UserRepository extends Repository<typeof UsersCollection> {
  constructor() {
    super(UsersCollection);
  }
}

Configuration

Default store:

document:
  backend: memory

Named stores:

document:
  backend: memory

  audit:
    backend: firestore
    projectId: my-project
    emulatorHost: 127.0.0.1:8080

Bind a collection to a named store:

Collection(
  'audit',
  {
    id: DocumentId(String),
    action: Field(String),
  },
  { db: 'audit' },
);

Repository API

const users = new UserRepository();

await users.save({
  id: 'user-123',
  email: 'alice@example.com',
  name: 'Alice',
  createdAt: new Date().toISOString(),
});

const user = await users.get('user-123');
const exists = await users.exists('user-123');

const result = await users.find(
  {
    email: { not: 'blocked@example.com' },
    createdAt: { exists: true },
  },
  {
    limit: 20,
    orderBy: { field: 'createdAt', direction: 'desc' },
  },
);

Available methods:

  • get(id, { consistency? })
  • exists(id, { consistency? })
  • findOne(filters, options?)
  • find(filters, { limit?, cursor?, orderBy?, consistency? })
  • save(document, { merge?, strict? })
  • saveMany(documents)
  • delete(id)
  • deleteMany(filters)

Query model

Portable operators:

  • equality: equals, not
  • comparison: gt, gte, lt, lte
  • set membership: in, notIn
  • array membership: contains
  • presence: exists

Ordering is structured, not SQL text:

await users.find({}, { orderBy: { field: 'email', direction: 'asc' } });

Pagination is cursor-based:

const firstPage = await users.find({}, { limit: 10, orderBy: { field: 'email', direction: 'asc' } });

const secondPage = await users.find({}, {
  limit: 10,
  cursor: firstPage.nextCursor,
  orderBy: { field: 'email', direction: 'asc' },
});

There is no offset API.

Save semantics

save() is replace-by-default:

await users.save({
  id: 'user-123',
  email: 'alice@example.com',
  name: 'Alice Updated',
});

Use merge: true for patch-style writes:

await users.save(
  {
    id: 'user-123',
    nickname: 'ally',
  },
  { merge: true },
);

Schema validation uses the same primitives exported by @putnami/runtime.

Transactions

import { runInTransaction } from '@putnami/document';

await runInTransaction(async () => {
  await users.save({ id: 'user-123', email: 'alice@example.com', name: 'Alice' });
  await users.save({ id: 'user-456', email: 'bob@example.com', name: 'Bob' });
});

Transactions are scoped to a single named store. Cross-store repository access inside a transaction throws TransactionNotSupported.

Index declarations

Collections can declare portable index coverage:

Collection(
  'users',
  {
    id: DocumentId(String),
    email: Field(String),
    createdAt: Field(DateIso),
  },
  {
    indexes: [
      { fields: ['email'] },
      { partition: ['email'], sort: ['createdAt'] },
    ],
  },
);

Enable strict index enforcement in config:

document:
  strictIndexes: true

Queries that are not covered by declared index metadata throw IndexMissing.

Backends

Memory

Good for tests and lightweight local use.

  • no external dependency
  • transactions supported
  • composite ids supported

Firestore

Good for managed remote document storage.

  • loaded through optional peer dependency
  • transactions supported
  • single scalar document id only
  • supports projectId, databaseId, emulatorHost, and credentials

Lifecycle And Observability

Use useBackend(), closeBackend(), and closeAllBackends() when you need manual backend control. Repositories also emit per-collection metrics and structured logs for get, find, save, saveMany, delete, and deleteMany.

See also

  • Persistence
  • Storage

On this page

  • Document Storage
  • Installation
  • Enable the plugin
  • Define collections
  • Create a repository
  • Configuration
  • Repository API
  • Query model
  • Save semantics
  • Transactions
  • Index declarations
  • Backends
  • Memory
  • Firestore
  • Lifecycle And Observability
  • See also