Adapters
The read shape both adapters share
A read returns the record's stored fields plus id. An adapter neither invents fields nor hides them, so the same record reads back the same way whichever backend holds it:
ts
{ id: "hero", title: "Hello", createdAt: "2026-09-06T…", updatedAt: "…" }idis the record's address, and so is the collection name. Neither is a field, andcollectionis not returned.createdAt/updatedAtare the adapter's own, written on every create and update, and both adapters return them.- Timestamps are ISO strings, not
Dateobjects, on both backends. These records are JSON-serialized acrosscreateContentHandlerand server-rendered payloads, where aDatebecomes a string anyway. - An adapter accepts back what it returned: handing a record you just read to
createWithIdorupsertworks, which is what makes copying one environment into another possible.
seedItemMap strips all four before writing, since they address the record or belong to the adapter rather than being content.
better-content/adapters/postgres
ts
class PostgresDataAdapter implements DataAdapter {
constructor(config: PostgresAdapterConfig);
}
interface PostgresAdapterConfig {
db?: PgDatabase; // a Drizzle database (any pg driver)
pool?: Pool; // or a pg Pool to build one from
connectionString?: string; // or a connection string
schema: Record<string, PgTable>; // collection name → Drizzle table (required)
}Typed-only Drizzle adapter. You declare tables with pgTable(...) and own migrations (Drizzle Kit); the adapter performs DML only.
- Throws on unregistered collections, undeclared fields (writes and filters), and unsupported query shapes.
containsmaps toILIKE '%value%';in/ninmap toinArray/notInArray; OR groups are supported.- Default ordering:
createdAtdescending when the column exists and noorderByis given. update/upsertsetupdatedAt = new Date().- Reads return
createdAt/updatedAtas ISO strings, and writes accept either those strings or aDate. creategenerates an id withcrypto.randomUUIDwhere available.pgand the node-postgres driver load lazily, only when the adapter must build its own pool; passingdbworks withoutpginstalled, including in browsers (PGlite).
Peers: drizzle-orm >= 0.40, pg >= 8 (pool path only).
better-content/adapters/firestore
ts
class FirestoreDataAdapter implements DataAdapter {
constructor(config?: FirestoreAdapterConfig);
}
interface FirestoreAdapterConfig {
db?: Firestore; // existing admin Firestore instance
credentials?: { // or service-account credentials
projectId?: string;
clientEmail?: string;
privateKey?: string;
databaseURL?: string;
};
defaultOrderByField?: string; // default "createdAt"
}Operator mapping: eq ==, ne !=, lt <, lte <=, gt >, gte >=, in in, nin not-in.
- Throws on
contains(no native substring search) and on OR filter groups; the errors say so explicitly. - Firestore Timestamps serialize to ISO strings in results.
create/createWithIdstampcreatedAtandupdatedAt;update/upsertrefreshupdatedAt(upsertmerges).createWithIduses Firestore'screate, so it rejects an id that already exists rather than overwriting it. Useupsertto write regardless, ordeletethencreateWithIdto replace a document.- Because reads order by
createdAtand Firestore omits documents that lack the ordering field, records written only throughupsertcan be missing from an unfiltered read.createWithIdstampscreatedAt, so seeding through it (or throughdelete+createWithId) avoids this. - With no query, reads order by
defaultOrderByFielddescending.
Peer: firebase-admin >= 12 (uses the modular firebase-admin/app and firebase-admin/firestore APIs).