Lucid storage
The production backend — how it resolves the connection, the three tables and their columns, what the published migrations do, and what actually needs backing up.
lucidStorage persists documents, versions and comments in your app's own database, through the
Lucid connection you name.
import { defineConfig, lucidStorage } from '@adonis-agora/collaboration'
export default defineConfig({
engine: 'yjs',
storage: lucidStorage({ connection: 'primary' }),
// ...
})The connection is resolved inside the library, lazily, at first use — there is no import db
in your config file and no database access during boot. That matters more than it looks: a config
file that touches the container at module scope is a config file that breaks in ace commands and
tests.
Omitting the connection uses the app's default
connection is optional: leave it out and the storage uses whatever db.connection() resolves to
— the same connection an unqualified Lucid query uses. Name it when collaboration data lives
somewhere other than your primary database.
The tables
| Table | Key | Holds |
|---|---|---|
collab_documents | doc_name | the encoded document state, plus free-form meta |
collab_versions | (doc_name, id) | version metadata and the snapshot bytes for that version |
collab_comments | id | anchored comments, indexed by (doc_name, space) |
Document state and version snapshots are stored as binary. Comment anchors are jsonb, so a
future anchor kind is a code change rather than a migration.
Migrations, and the tables that create themselves
node ace collaboration:init publishes a migration per table. Run them like any other:
node ace migration:runThe storage also issues CREATE TABLE IF NOT EXISTS before its first query, so a fresh clone
works before anyone has migrated. Both paths create the same schema — but ship the migrations
anyway. They are what makes the schema reviewable in a pull request, versioned with the code, and
reversible. The lazy creation is there so development does not stall, not so you can skip it.
Reading the data yourself
These are ordinary tables. Nothing stops you from querying them, and some things are much easier there than through the manager:
// Every document a user has commented on, without opening any of them
const docs = await db
.from('collab_comments')
.where('user_id', user.id)
.distinct('doc_name')Housekeeping on the version history has its own API — collaboration:prune --dry-run reports what a
retention run would delete, and pruneVersions({ keep }) does it from code. Do not write the DELETE
yourself.
Reading collab_documents.state is the one exception. Those bytes are a CRDT encoding, not
your content — decoding them by hand means reimplementing Yjs. Ask the manager instead, which also
gets you the live version when someone has the document open:
const text = await collaboration.current.getDocumentText({ docName })
const bytes = await collaboration.current.getDocumentState({ docName })Backups
collab_documents is the one table whose loss is unrecoverable — it is the content. Versions are
recoverable in the sense that the document survives without them; comments are ordinary rows.
Two properties worth designing around:
- Row size grows with the document. A long manuscript is a large binary column, rewritten on
every debounce window. Watch the write amplification before you lower
debounceon a table that is already large. - Versions multiply that. Each one stores its own snapshot. Deliberate, user-created checkpoints are fine; an automatic version every minute is a storage plan, not a feature. See Production.
Storage
The one persistence seam every driver shares — what CollaborationStorage is responsible for, the three built-in implementations, and how to pick between them.
Custom storage
Implement CollaborationStorage yourself — the ten methods, the contracts that are easy to get subtly wrong, and a worked S3-backed example.