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)
Auto-Discovery (Recommended)
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 fieldAPI 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 appsorgId- Multi-tenant appsworkspaceId- Workspace-based appstenantId- 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)vrevision
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 userUse 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
ownershipFieldsarray - Check model isn’t in
excludelist - 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
orgIdbeing passed matches record’s ownership field - Ensure auth function is setting correct
req.user
Next Steps
- Authentication - Secure your endpoints
- Custom Adapters - Build your own
- Batch Loading - Optimize lazy collections
- Performance - Tune for production