Skip to Content
GuidesServer GuidesDatabase Adapters

Database Adapters

Gluonic works with any database through the adapter pattern.

PrismaAdapter

The Prisma adapter supports all Prisma-compatible databases.

Supported databases:

  • PostgreSQL, MySQL, SQLite, SQL Server, MongoDB, CockroachDB

Package: @gluonic/server-prisma (available now)

Let Gluonic auto-discover your models:

import { PrismaAdapter } from '@gluonic/server-prisma' const database = PrismaAdapter({ prisma, autoDiscover: { enabled: true, ownershipFields: ['orgId', 'workspaceId', 'tenantId', 'ownerId'], // Default versionField: 'version', // Default exclude: ['Session', '_prisma_migrations'] } })

Console output:

[gluonic] ✓ Auto-discovered model: user (orgId, version) [gluonic] ✓ Auto-discovered model: post (orgId, version) [gluonic] ✓ Auto-discovered model: tag (version only - public model) [gluonic] ⊘ Skipping Session - missing version field

API Reference: PrismaAdapter - Complete configuration options

Manual Configuration

For full control, specify models explicitly:

const database = PrismaAdapter({ prisma, models: { user: { ownership: 'orgId', versioning: 'version' }, post: { ownership: 'orgId', versioning: 'version', batchIndexKeys: ['authorId', 'categoryId'] // Enable batch loading }, tag: { // Public model (no ownership filtering) versioning: 'version' } } })

Custom Adapters

Create adapters for other ORMs or databases.

TypeORM Adapter (Example)

import { DatabaseAdapter } from '@gluonic/core' import { DataSource } from 'typeorm' export function TypeORMAdapter(config: { dataSource: DataSource models: Record<string, { entity: any ownership: string versioning: string }> }): DatabaseAdapter { return { async bootstrap(orgId: string) { const rows = [] for (const [modelName, modelCfg] of Object.entries(config.models)) { const repo = config.dataSource.getRepository(modelCfg.entity) const records = await repo.find({ where: { [modelCfg.ownership]: orgId } }) for (const record of records) { rows.push({ t: modelName, id: String(record.id), v: Number(record[modelCfg.versioning]), p: serializeRecord(record) }) } } return rows }, async applyMutations(orgId, userId, mutations) { // Implement mutations with ownership/version checks // See: /guides/advanced/custom-adapters }, async loadSyncActions(orgId, since, limit) { // Load sync actions for delta }, async writeSyncAction(orgId, action) { // Write to SyncAction table }, async getLastSyncId(orgId) { // Get latest sync ID } } }

MongoDB Adapter

import { DatabaseAdapter } from '@gluonic/server' import { MongoClient } from 'mongodb' export function createMongoAdapter(config: { client: MongoClient database: string models: Record<string, { collection: string ownership: string versioning: string }> }): DatabaseAdapter { const db = config.client.db(config.database) return { async bootstrap(orgId: string) { const rows = [] for (const [modelName, modelCfg] of Object.entries(config.models)) { const collection = db.collection(modelCfg.collection) const docs = await collection.find({ [modelCfg.ownership]: orgId }).toArray() for (const doc of docs) { rows.push({ t: modelName, id: String(doc._id), v: Number(doc[modelCfg.versioning] || 0), p: serializeDocument(doc) }) } } return rows }, // Implement other methods... } }

Raw SQL Adapter

import { DatabaseAdapter } from '@gluonic/server' import pg from 'pg' export function createPostgreSQLAdapter(config: { pool: pg.Pool models: Record<string, { table: string ownership: string versioning: string }> }): DatabaseAdapter { return { async bootstrap(orgId: string) { const rows = [] for (const [modelName, modelCfg] of Object.entries(config.models)) { const result = await config.pool.query( `SELECT * FROM ${modelCfg.table} WHERE ${modelCfg.ownership} = $1`, [orgId] ) for (const row of result.rows) { rows.push({ t: modelName, id: String(row.id), v: Number(row[modelCfg.versioning]), p: row }) } } return rows }, // Implement other methods... } }

Adapter Interface

All adapters must implement:

interface DatabaseAdapter { // Required: Core operations bootstrap(orgId: string): Promise<WireRow[]> applyMutations(orgId: string, userId: string, mutations: Mutation[]): Promise<MutationResult[]> loadSyncActions(orgId: string, since: number, limit: number): Promise<SyncAction[]> writeSyncAction(orgId: string, action: SyncActionInput): Promise<number> getLastSyncId(orgId: string): Promise<number> // Optional: Advanced features batchFetch?(orgId: string, keys: BatchKey[]): Promise<Record<string, WireRow[]>> streamBootstrap?(orgId: string): AsyncIterable<WireRow> getMetadata?(): { name: string; version: string; models: string[] } }

Field Naming Conventions

Ownership Field

The field that stores which organization/user owns this record:

Common names:

  • userId - Single-user apps
  • orgId - Multi-tenant apps
  • workspaceId - Workspace-based apps
  • tenantId - Enterprise apps

Behavior:

  • Used for filtering bootstrap (only load user’s data)
  • Used for mutation authorization (can only edit own data)
  • Optional (omit for public models)

Versioning Field

The field that stores the version number for optimistic concurrency:

Common names:

  • version (recommended)
  • v
  • revision

Behavior:

  • Increments on every update
  • Used for conflict detection
  • Required for all synced models

Serialization

Adapters handle automatic serialization:

const adapter = PrismaAdapter({ prisma, serialization: { // Auto-convert by convention dateFields: ['*At'], // createdAt, updatedAt → timestamps bigIntFields: ['*Time'], // startTime, endTime → strings // Or explicit dateFields: ['createdAt', 'updatedAt', 'publishedAt'], bigIntFields: ['timestamp', 'eventTime'], // Exclude sensitive fields exclude: ['password', 'secretKey'] } })

Public Models

Models without ownership are public (everyone can read/write):

const adapter = PrismaAdapter({ prisma, models: { tag: { // No ownership field! versioning: 'version' } } }) // Bootstrap returns ALL tags (not filtered by user) // Mutations allowed from any user

Use cases:

  • Tags, categories (shared across users)
  • Configuration data
  • Reference data

Security note: Use authorization adapter for read-only public data (coming soon).

Testing Your Adapter

// Test bootstrap const rows = await adapter.bootstrap('test-org-id') console.log('Bootstrap:', rows.length, 'rows') // Test mutations const results = await adapter.applyMutations('test-org-id', 'test-user-id', [ { type: 'post', id: 'post-1', op: 'i', patch: { title: 'Test' } } ]) console.log('Mutation result:', results[0]) // Test delta const actions = await adapter.loadSyncActions('test-org-id', 0, 10) console.log('Sync actions:', actions.length)

Troubleshooting

Models not auto-discovered

Problem: [gluonic] No models configured for sync

Solutions:

  • Check models have both ownership AND version fields
  • Verify ownership field name matches ownershipFields array
  • Check model isn’t in exclude list
  • Run Prisma migration to ensure schema is up-to-date

Serialization errors

Problem: Cannot serialize Date object

Solution:

serialization: { dateFields: ['createdAt', 'updatedAt'] // Auto-convert to timestamps }

Mutation ownership errors

Problem: { ok: false, error: 'forbidden' }

Solution:

  • Verify ownership field is set correctly in model
  • Check orgId being passed matches record’s ownership field
  • Ensure auth function is setting correct req.user

Next Steps

Last updated on