{"id":31920273,"url":"https://github.com/foretaginc/tanstack-db-surrealdb","last_synced_at":"2026-05-11T02:29:17.931Z","repository":{"id":317806659,"uuid":"1068581383","full_name":"ForetagInc/tanstack-db-surrealdb","owner":"ForetagInc","description":"Opinionated SurrealDB collections for Tanstack DB for Local First apps with CRDTs","archived":false,"fork":false,"pushed_at":"2026-02-24T05:42:48.000Z","size":230,"stargazers_count":6,"open_issues_count":2,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-02-24T12:20:00.863Z","etag":null,"topics":["loro-crdt","surrealdb","tanstack-db"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"unlicense","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ForetagInc.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"open_collective":"foretag"}},"created_at":"2025-10-02T15:43:57.000Z","updated_at":"2026-02-24T05:42:37.000Z","dependencies_parsed_at":"2026-01-10T20:02:09.872Z","dependency_job_id":null,"html_url":"https://github.com/ForetagInc/tanstack-db-surrealdb","commit_stats":null,"previous_names":["foretaginc/tanstack-db-surrealdb"],"tags_count":61,"template":false,"template_full_name":null,"purl":"pkg:github/ForetagInc/tanstack-db-surrealdb","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ForetagInc%2Ftanstack-db-surrealdb","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ForetagInc%2Ftanstack-db-surrealdb/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ForetagInc%2Ftanstack-db-surrealdb/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ForetagInc%2Ftanstack-db-surrealdb/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ForetagInc","download_url":"https://codeload.github.com/ForetagInc/tanstack-db-surrealdb/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ForetagInc%2Ftanstack-db-surrealdb/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29935167,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-28T13:00:17.143Z","status":"ssl_error","status_checked_at":"2026-02-28T12:59:13.669Z","response_time":90,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["loro-crdt","surrealdb","tanstack-db"],"created_at":"2025-10-13T21:57:49.685Z","updated_at":"2026-05-11T02:29:17.924Z","avatar_url":"https://github.com/ForetagInc.png","language":"TypeScript","funding_links":["https://opencollective.com/foretag"],"categories":[],"sub_categories":[],"readme":"# @foretag/tanstack-db-surrealdb\n\nTanStack DB collection adapter for SurrealDB JS with:\n\n- Realtime replication (`LIVE`)\n- Local-first writes\n- Optional E2EE envelopes (`version/algorithm/key_id/nonce/ciphertext`)\n- Optional Loro CRDT replication (`json`, `richtext`)\n- Query-driven sync modes (`eager`, `on-demand`, `progressive`)\n\n## Install\n\n```sh\nnpm install @foretag/tanstack-db-surrealdb\n# or\nbun add @foretag/tanstack-db-surrealdb\n```\n\n## Quick Start\n\n```ts\nimport { createCollection } from '@tanstack/db';\nimport { QueryClient } from '@tanstack/query-core';\nimport { Surreal } from 'surrealdb';\nimport { surrealCollectionOptions } from '@foretag/tanstack-db-surrealdb';\n\nconst db = new Surreal();\nconst queryClient = new QueryClient();\n\ntype Product = { id: string; name: string; price: number };\n\nexport const products = createCollection(\n\tsurrealCollectionOptions\u003cProduct\u003e({\n\t\tdb,\n\t\ttable: { name: 'product' },\n\t\tqueryClient,\n\t\tqueryKey: ['product'],\n\t\tsyncMode: 'eager',\n\t}),\n);\n```\n\n## Persistence\n\nTanStack DB persistence can wrap this adapter directly. The adapter now returns a\nstable collection `id` by default using:\n\n```ts\nsurreal:${tableName}:${hashKey(queryKey)}\n```\n\nThat makes it safe to compose with `persistedCollectionOptions(...)` across\nrestarts. If you need a custom persistence boundary, pass `id` explicitly and it\nwill be preserved.\n\nIf you want less boilerplate, use `persistedSurrealCollectionOptions(...)` from\nthis package and pass the runtime-specific `persistence` adapter plus\n`schemaVersion` directly.\n\nCreate the SQLite database and persistence adapter once per app/runtime, export\nthat shared `persistence`, and reuse it across every persisted collection in\nthe app.\n\n```ts\n// Create once, reuse everywhere\nconst sqlite = await openBrowserWASQLiteOPFSDatabase({\n\tdatabaseName: 'tanstack-db.sqlite',\n});\n\nexport const persistence = createBrowserWASQLitePersistence({\n\tdatabase: sqlite,\n});\n```\n\nBrowser-first example:\n\n```ts\n// persistence.ts\nimport { createCollection } from '@tanstack/db';\nimport { QueryClient } from '@tanstack/query-core';\nimport {\n  createBrowserWASQLitePersistence,\n  openBrowserWASQLiteOPFSDatabase,\n} from '@tanstack/browser-db-sqlite-persistence';\nimport { Surreal } from 'surrealdb';\nimport { persistedSurrealCollectionOptions } from '@foretag/tanstack-db-surrealdb';\n\nconst db = new Surreal();\nconst queryClient = new QueryClient();\n\nconst sqlite = await openBrowserWASQLiteOPFSDatabase({\n  databaseName: 'tanstack-db.sqlite',\n});\n\nexport const persistence = createBrowserWASQLitePersistence({\n  database: sqlite,\n});\n\ntype Product = { id: string; name: string; price: number };\ntype Category = { id: string; name: string };\n\nexport const products = createCollection(\n\tpersistedSurrealCollectionOptions\u003cProduct\u003e({\n\t\tpersistence,\n\t\tschemaVersion: 1,\n\t\tdb,\n\t\ttable: { name: 'product' },\n\t\tqueryClient,\n\t\tqueryKey: ['product'],\n\t\tsyncMode: 'eager',\n\t}),\n);\n\nexport const categories = createCollection(\n\tpersistedSurrealCollectionOptions\u003cCategory\u003e({\n\t\tpersistence,\n\t\tschemaVersion: 1,\n\t\tdb,\n\t\ttable: { name: 'category' },\n\t\tqueryClient,\n\t\tqueryKey: ['category'],\n\t\tsyncMode: 'eager',\n\t}),\n);\n```\n\nYou only need one `openBrowserWASQLiteOPFSDatabase(...)` call and one\n`createBrowserWASQLitePersistence(...)` call per browser app, not per\ncollection\n\n## Adapter API\n\n```ts\ntype SurrealCollectionOptions\u003cT\u003e = {\n\tid?: string;\n\tdb: Surreal;\n\ttable: Table | { name: string; relation?: boolean } | string;\n\tqueryClient: QueryClient;\n\tqueryKey: readonly unknown[];\n\tsyncMode?: 'eager' | 'on-demand' | 'progressive';\n\te2ee?: {\n\t\tenabled: boolean;\n\t\tcrypto: CryptoProvider;\n\t\taad?: (ctx: { table: string; id: string; kind: 'base'|'update'|'snapshot'; baseTable?: string }) =\u003e Uint8Array;\n\t};\n\tcrdt?: {\n\t\tenabled: boolean;\n\t\tprofile: 'json' | 'richtext';\n\t\tupdatesTable: Table | { name: string } | string;\n\t\tsnapshotsTable?: Table | { name: string } | string;\n\t\t// Optional overrides. If omitted, adapter uses built-in handlers for `profile`.\n\t\tmaterialize?: (doc: LoroDoc, id: string) =\u003e T;\n\t\tapplyLocalChange?: (doc: LoroDoc, change: { type: 'insert'|'update'|'delete'; value: T }) =\u003e void;\n\t\tpersistMaterializedView?: boolean;\n\t\tactor?: string | ((ctx: { id: string; change?: { type: 'insert'|'update'|'delete'; value: T } }) =\u003e string | undefined);\n\t\tlocalActorId?: string; // deprecated\n\t};\n};\n```\n\n`id` is optional. When omitted, the adapter derives a stable collection id from\nthe Surreal table name and `queryKey` so TanStack DB persistence wrappers can\nreuse the same persisted collection state across restarts.\n\n## E2EE\n\nEnvelope fields stored in Surreal records:\n\n```ts\ntype EncryptedEnvelope = {\n  version: number;\n  algorithm: string;\n  key_id: string;\n  nonce: string;\n  ciphertext: string;\n};\n```\n\nDefault AAD:\n\n- Base records: `\u003ctable\u003e:\u003crecord_id\u003e`\n- CRDT updates/snapshots: `\u003cupdates_or_snapshots_table\u003e:\u003cbase_table\u003e:\u003cdoc_id\u003e`\n\nIncluded provider:\n\n- `WebCryptoAESGCM` (`AES-256-GCM`, versioned envelope)\n\n## CRDT Profiles\n\nCRDT is managed by profile by default:\n\n- `profile: 'json'` uses built-in JSON handlers\n- `profile: 'richtext'` uses built-in richtext handlers\n\nAdvanced overrides are still available:\n\n- `createLoroProfile('json' | 'richtext')`\n- `materialize` and `applyLocalChange` in `crdt` options\n\nFor CRDT loop-prevention metadata, prefer `crdt.actor` so actor identity can be resolved per doc/write. `localActorId` remains only for backwards compatibility.\n\n## CRDT Table Requirements\n\nFor `crdt.enabled: true`, users must provide:\n\n- Base table (`table`) for record identity and optional materialized metadata.\n- Updates table (`crdt.updatesTable`) as append-only CRDT log.\n\nOptional:\n\n- Snapshots table (`crdt.snapshotsTable`) for compaction and faster hydration.\n\nIf `crdt.updatesTable` is missing, CRDT mode cannot function.\n\n## SQL Templates\n\n### Plain\n\n```sql\nDEFINE TABLE note SCHEMAFULL;\nDEFINE FIELD title ON note TYPE string;\nDEFINE FIELD body ON note TYPE string;\nDEFINE FIELD updated_at ON note TYPE datetime VALUE time::now();\nDEFINE INDEX note_updated ON note FIELDS updated_at;\n```\n\n### E2EE-only\n\n```sql\nDEFINE TABLE secret_note SCHEMAFULL;\nDEFINE FIELD owner ON secret_note TYPE record\u003caccount\u003e;\nDEFINE FIELD updated_at ON secret_note TYPE datetime;\nDEFINE FIELD version ON secret_note TYPE int;\nDEFINE FIELD algorithm ON secret_note TYPE string;\nDEFINE FIELD key_id ON secret_note TYPE string;\nDEFINE FIELD nonce ON secret_note TYPE string;\nDEFINE FIELD ciphertext ON secret_note TYPE string;\nDEFINE INDEX secret_note_owner_updated ON secret_note FIELDS owner, updated_at;\n```\n\n### CRDT-only\n\n```sql\nDEFINE TABLE doc SCHEMAFULL;\nDEFINE FIELD owner ON doc TYPE record\u003caccount\u003e;\nDEFINE FIELD updated_at ON doc TYPE datetime;\nDEFINE INDEX doc_owner_updated ON doc FIELDS owner, updated_at;\n\n-- Necessary for CRDT updates\nDEFINE TABLE crdt_update SCHEMAFULL;\nDEFINE FIELD doc ON crdt_update TYPE record\u003cdoc\u003e;\nDEFINE FIELD ts ON crdt_update TYPE datetime;\nDEFINE FIELD update_bytes ON crdt_update TYPE string;\nDEFINE FIELD actor ON crdt_update TYPE string;\nDEFINE INDEX crdt_doc_ts ON crdt_update FIELDS doc, ts;\n\nDEFINE TABLE crdt_snapshot SCHEMAFULL;\nDEFINE FIELD doc ON crdt_snapshot TYPE record\u003cdoc\u003e;\nDEFINE FIELD ts ON crdt_snapshot TYPE datetime;\nDEFINE FIELD snapshot_bytes ON crdt_snapshot TYPE string;\nDEFINE INDEX snap_doc_ts ON crdt_snapshot FIELDS doc, ts;\n```\n\n### CRDT + E2EE\n\n```sql\nDEFINE TABLE secure_doc SCHEMAFULL;\nDEFINE FIELD owner ON secure_doc TYPE record\u003caccount\u003e;\nDEFINE FIELD updated_at ON secure_doc TYPE datetime;\nDEFINE INDEX secure_doc_owner_updated ON secure_doc FIELDS owner, updated_at;\n\nDEFINE TABLE crdt_update SCHEMAFULL;\nDEFINE FIELD doc ON crdt_update TYPE record\u003csecure_doc\u003e;\nDEFINE FIELD ts ON crdt_update TYPE datetime;\nDEFINE FIELD actor ON crdt_update TYPE string;\nDEFINE FIELD version ON crdt_update TYPE int;\nDEFINE FIELD algorithm ON crdt_update TYPE string;\nDEFINE FIELD key_id ON crdt_update TYPE string;\nDEFINE FIELD nonce ON crdt_update TYPE string;\nDEFINE FIELD ciphertext ON crdt_update TYPE string;\nDEFINE INDEX crdt_doc_ts ON crdt_update FIELDS doc, ts;\n```\n\nIf a single `crdt_update` table is shared across multiple base tables, use a union type such as `record\u003cdoc\u003e | record\u003csheet\u003e`.\n\n## Permissions Templates\n\nThe adapter does not manage Surreal table permissions. Define them in schema.\n\n### E2EE-only table permissions\n\n```sql\nDEFINE TABLE secret_note SCHEMAFULL\n\tPERMISSIONS\n\t\tFOR select, create, update, delete WHERE owner = $auth.id;\n```\n\n### CRDT updates table permissions (append-only)\n\n```sql\nDEFINE TABLE crdt_update SCHEMAFULL\n\tPERMISSIONS\n\t\tFOR select, create WHERE owner = $auth.id\n\t\tFOR update, delete NONE;\n\n-- Add owner metadata on update rows for simple ACL checks\nDEFINE FIELD owner ON crdt_update TYPE record\u003caccount\u003e;\nDEFINE INDEX crdt_owner_doc_ts ON crdt_update FIELDS owner, doc, ts;\n```\n\n### CRDT snapshots table permissions\n\n```sql\nDEFINE TABLE crdt_snapshot SCHEMAFULL\n\tPERMISSIONS\n\t\tFOR select WHERE owner = $auth.id\n\t\tFOR create, update, delete NONE;\n\n-- Common pattern: clients read snapshots; only trusted backend writes/prunes them\nDEFINE FIELD owner ON crdt_snapshot TYPE record\u003caccount\u003e;\nDEFINE INDEX snap_owner_doc_ts ON crdt_snapshot FIELDS owner, doc, ts;\n```\n\nIf you run snapshot compaction from a trusted backend/service account, grant create/delete to that account only.\n\n## Usage Snippets\n\n### E2EE-only secret table\n\n```ts\nconst provider = await WebCryptoAESGCM.fromRawKey(rawKey, { kid: 'org-key-2026-01' });\n\nconst secrets = createCollection(\n\tsurrealCollectionOptions\u003c{ id: string; title: string; body: string }\u003e({\n\t\tdb,\n\t\ttable: { name: 'secret_note' },\n\t\tqueryClient,\n\t\tqueryKey: ['secret-note'],\n\t\tsyncMode: 'eager',\n\t\te2ee: { enabled: true, crypto: provider },\n\t}),\n);\n```\n\n### CRDT richtext docs\n\n```ts\nconst docs = createCollection(\n\tsurrealCollectionOptions\u003c{ id: string; content: string; title?: string }\u003e({\n\t\tdb,\n\t\ttable: { name: 'doc' },\n\t\tqueryClient,\n\t\tqueryKey: ['doc'],\n\t\tsyncMode: 'on-demand',\n\t\tcrdt: {\n\t\t\tenabled: true,\n\t\t\tprofile: 'richtext',\n\t\t\tupdatesTable: { name: 'crdt_update' },\n\t\t\tsnapshotsTable: { name: 'crdt_snapshot' },\n\t\t\tactor: ({ id }) =\u003e id.startsWith('team-a') ? 'device:team-a:abc' : 'device:team-b:abc',\n\t\t},\n\t}),\n);\n```\n\n### RecordId model example\n\n```ts\nimport { RecordId } from 'surrealdb';\n\ntype CalendarEvent = {\n\tid: RecordId\u003c'calendar_event'\u003e;\n\towner: RecordId\u003c'account'\u003e;\n\ttitle: string;\n\tstart_at: string;\n};\n\nawait calendarEvents.insert({\n\t// id is Optional on insert\n\tid: new RecordId('calendar_event', 'evt-001'),\n\towner: new RecordId('account', 'user-123'),\n\ttitle: 'Planning',\n\tstart_at: '2026-02-23T10:00:00.000Z',\n});\n```\n\nFull runnable example: `examples/record-id.ts`.\n\n### On-demand drive listing (query-driven)\n\n```ts\nimport { createLiveQueryCollection, eq } from '@tanstack/db';\n\nconst files = createCollection(\n\tsurrealCollectionOptions\u003c{ id: string; owner: string; updated_at: string; name: string }\u003e({\n\t\tdb,\n\t\ttable: { name: 'file' },\n\t\tqueryClient,\n\t\tqueryKey: ['file'],\n\t\tsyncMode: 'on-demand',\n\t}),\n);\n\nconst ownerFiles = createLiveQueryCollection((q) =\u003e\n\tq\n\t\t.from({ files })\n\t\t.where(({ files }) =\u003e eq(files.owner, 'account:abc'))\n\t\t.select(({ files }) =\u003e files),\n);\n\nawait ownerFiles.preload();\n```\n\n## Key Wrapping / Multi-Principal Access\n\nThis adapter expects key management to be provided by your app or KMS. For production shared access (users, teams, orgs), keep using wrapped keys:\n\n- Encrypt entity data with a data key.\n- Wrap that data key for each authorized principal (user/team/service/device).\n- Resolve the active key by `kid` at decrypt time.\n- Rotate by issuing a new `kid` and re-wrapping/re-encrypting progressively.\n\nThe adapter consumes derived keys through `CryptoProvider`; it does not manage wrapping policy for you.\n\n## Testing\n\nUnit tests (`bun test`) cover:\n\n- id/query translation behavior\n- modern eager + on-demand sync controls\n- E2EE envelope/AAD behavior\n- CRDT update append, snapshot hydration, and actor loop prevention\n\nReal SurrealDB integration tests are available and run against a live instance:\n\n1. Copy `.env.example` to `.env` and fill connection/auth values.\n2. Run `bun run test:integration`.\n\nRequired env:\n\n- `SURREAL_URL`\n- `SURREAL_NAMESPACE`\n- `SURREAL_DATABASE`\n- `SURREAL_USERNAME`\n- `SURREAL_PASSWORD`\n\n`SURREAL_REQUIRE_LIVE=true` (default) enforces LIVE query assertions; set it to `false` if you intentionally use a connection without LIVE support.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fforetaginc%2Ftanstack-db-surrealdb","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fforetaginc%2Ftanstack-db-surrealdb","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fforetaginc%2Ftanstack-db-surrealdb/lists"}