Core API Reference
@korajs/core is the foundation of every Kora.js application. It defines the schema system, operation model, hybrid logical clock, and shared types. It has zero dependencies on other @kora packages.
All exports documented here are also available from the kora meta-package.
import { defineSchema, t, op, migrate, HybridLogicalClock, createOperation, KoraError } from '@korajs/core'
// or
import { defineSchema, t, op, migrate, HybridLogicalClock, createOperation, KoraError } from 'korajs'defineSchema()
Creates a validated schema definition that describes your application's data model. This is the primary entry point for configuring a Kora application.
Signature
function defineSchema(input: SchemaInput): SchemaDefinitionParameters
| Parameter | Type | Description |
|---|---|---|
input | SchemaInput | Schema configuration object |
SchemaInput
| Field | Type | Required | Description |
|---|---|---|---|
version | number | Yes | Schema version number. Must be a positive integer. Increment when you make changes. |
collections | Record<string, CollectionDefinition> | Yes | Map of collection names to their definitions. |
relations | Record<string, RelationDefinition> | No | Map of relation names to their definitions. |
CollectionDefinition
| Field | Type | Required | Description |
|---|---|---|---|
fields | Record<string, FieldDescriptor> | Yes | Map of field names to type descriptors built with t. |
indexes | string[] | No | Fields to index for faster queries. |
constraints | Record<string, Constraint> | No | Tier 2 constraint definitions for conflict resolution. |
resolve | Record<string, ResolverFn> | No | Tier 3 custom resolver functions for specific fields. |
RelationDefinition
| Field | Type | Required | Description |
|---|---|---|---|
from | string | Yes | Source collection name. |
to | string | Yes | Target collection name. |
type | 'many-to-one' | 'one-to-many' | 'many-to-many' | Yes | Relationship cardinality. |
field | string | Yes | Foreign key field on the source collection. |
onDelete | 'set-null' | 'cascade' | 'restrict' | 'no-action' | No | Behavior when the referenced record is deleted. Defaults to 'no-action'. |
Returns
SchemaDefinition -- A validated, frozen schema object used by createApp and other Kora internals.
Example
import { defineSchema, t } from 'korajs'
const schema = defineSchema({
version: 1,
collections: {
todos: {
fields: {
title: t.string(),
completed: t.boolean().default(false),
assignee: t.string().optional(),
tags: t.array(t.string()).default([]),
notes: t.richtext(),
priority: t.enum(['low', 'medium', 'high']).default('medium'),
dueDate: t.timestamp().optional(),
createdAt: t.timestamp().auto(),
},
indexes: ['assignee', 'completed', 'dueDate'],
},
projects: {
fields: {
name: t.string(),
color: t.string().default('#3b82f6'),
createdAt: t.timestamp().auto(),
},
},
},
relations: {
todoBelongsToProject: {
from: 'todos',
to: 'projects',
type: 'many-to-one',
field: 'projectId',
onDelete: 'set-null',
},
},
})Errors
- Throws
KoraErrorwith codeINVALID_SCHEMAifversionis not a positive integer. - Throws
KoraErrorwith codeINVALID_SCHEMAif a collection name is empty or contains invalid characters. - Throws
KoraErrorwith codeINVALID_SCHEMAif a relation references a collection that does not exist. - Throws
KoraErrorwith codeINVALID_SCHEMAif a relation references a field that does not exist on the source collection.
t (Type Builders)
The t object provides builder methods for defining field types in your schema. Each method returns a FieldDescriptor that can be further configured with modifier methods.
import { t } from 'korajs't.string()
Defines a text field. Stored as TEXT in SQLite.
t.string() // Required string field
t.string().optional() // Optional (nullable) string field
t.string().default('hello') // Defaults to 'hello' on insertt.number()
Defines a numeric field. Stored as REAL in SQLite.
t.number() // Required number field
t.number().optional() // Optional (nullable) number field
t.number().default(0) // Defaults to 0 on insertt.boolean()
Defines a boolean field. Stored as INTEGER (0/1) in SQLite.
t.boolean() // Required boolean field
t.boolean().default(false) // Defaults to false on insertt.enum(values)
Defines a field constrained to a set of string values. Stored as TEXT with a CHECK constraint in SQLite.
| Parameter | Type | Description |
|---|---|---|
values | readonly string[] | Allowed values for this field. |
t.enum(['low', 'medium', 'high']) // Required enum field
t.enum(['low', 'medium', 'high']).default('medium') // Defaults to 'medium't.timestamp()
Defines a timestamp field. Stored as INTEGER (milliseconds since epoch) in SQLite.
t.timestamp() // Required timestamp field
t.timestamp().optional() // Optional timestamp field
t.timestamp().auto() // Automatically set on insert (not user-writable)t.array(inner)
Defines an array field. Stored as TEXT (JSON-serialized) in SQLite. Uses add-wins set semantics during merge -- concurrent additions from different devices are both preserved.
| Parameter | Type | Description |
|---|---|---|
inner | FieldDescriptor | Type descriptor for array elements. |
t.array(t.string()) // Array of strings
t.array(t.number()).default([]) // Array of numbers, defaults to emptyt.richtext()
Defines a rich text field backed by a Yjs Y.Text CRDT. Stored as BLOB (Yjs state vector) in SQLite. Supports character-level collaborative editing with automatic merge.
t.richtext() // Rich text fieldRich text fields cannot use .default() or .optional() modifiers. They are always initialized as empty Y.Text documents.
Field modifiers
All type builders (except t.richtext()) support these chainable modifiers:
| Modifier | Description |
|---|---|
.optional() | Makes the field nullable. Omitted fields default to null. |
.default(value) | Sets a default value applied on insert when the field is not provided. |
.auto() | Field is set automatically by Kora (e.g., createdAt). Cannot be provided by the developer. Only valid on t.timestamp(). |
.merge(strategy) | Declares the merge strategy for this field during conflict resolution. See Conflict Resolution. Only valid on t.number(), t.string(), and t.array(). |
Modifiers return a new FieldDescriptor and can be chained:
t.string().optional() // Valid
t.number().default(0) // Valid
t.timestamp().auto() // Valid
t.string().optional().default('n/a') // Valid -- optional with a default.merge(strategy)
Declares how this field should be merged when concurrent edits conflict. Overrides the default Tier 1 auto-merge strategy.
| Parameter | Type | Description |
|---|---|---|
strategy | FieldMergeStrategy | The merge strategy to use. |
Available strategies
| Strategy | Valid on | Behavior |
|---|---|---|
'lww' | All scalar types | Last-Write-Wins (default for scalars) |
'counter' | t.number() | Additive merge: both deltas are applied to the base value |
'max' | t.number() | Keeps the highest value across all sides |
'min' | t.number() | Keeps the lowest value across all sides |
'union' | t.array() | Add-wins set (default for arrays) |
'append-only' | t.array() | Append-only: additions are kept, removals are ignored |
'server-authoritative' | All types | Remote/server value always wins |
import { defineSchema, t } from 'korajs'
const schema = defineSchema({
version: 1,
collections: {
products: {
fields: {
name: t.string(),
quantity: t.number().merge('counter'), // additive - both decrements apply
highScore: t.number().merge('max'), // keep the highest value
tags: t.array(t.string()).merge('append-only'), // never lose tags
status: t.string().merge('server-authoritative'), // server decides
},
},
},
})TIP
Schema-level merge strategies replace Tier 3 custom resolvers for common patterns like counters, max/min, and append-only lists. Use .merge() when a built-in strategy fits; use Tier 3 resolve functions for complex domain logic.
HybridLogicalClock
Implements the Hybrid Logical Clock algorithm (Kulkarni et al.) for causal ordering of operations across distributed devices without requiring synchronized clocks.
Constructor
new HybridLogicalClock(nodeId: string)| Parameter | Type | Description |
|---|---|---|
nodeId | string | Unique identifier for this device/node. Typically a UUID v7. |
Methods
.now()
Generates a new timestamp for a local event. Each call returns a strictly greater timestamp than the previous one.
now(): HLCTimestampReturns: HLCTimestamp -- A new timestamp with the current wall time (or incremented logical counter if wall time has not advanced).
const clock = new HybridLogicalClock('node-abc-123')
const ts1 = clock.now() // { wallTime: 1712188800000, logical: 0, nodeId: 'node-abc-123' }
const ts2 = clock.now() // { wallTime: 1712188800000, logical: 1, nodeId: 'node-abc-123' }.receive(remote)
Updates the local clock after receiving a remote timestamp. Ensures the local clock stays ahead of both its own previous value and the remote value.
receive(remote: HLCTimestamp): HLCTimestamp| Parameter | Type | Description |
|---|---|---|
remote | HLCTimestamp | Timestamp received from a remote node. |
Returns: HLCTimestamp -- The updated local timestamp after merging with the remote clock.
const localClock = new HybridLogicalClock('node-a')
const remoteTimestamp: HLCTimestamp = {
wallTime: 1712188900000,
logical: 5,
nodeId: 'node-b'
}
const updated = localClock.receive(remoteTimestamp)
// updated.wallTime >= remoteTimestamp.wallTimeHybridLogicalClock.compare(a, b)
Static method. Compares two timestamps for total ordering.
static compare(a: HLCTimestamp, b: HLCTimestamp): number| Parameter | Type | Description |
|---|---|---|
a | HLCTimestamp | First timestamp. |
b | HLCTimestamp | Second timestamp. |
Returns: number
- Negative if
ais beforeb - Positive if
ais afterb - Zero if
aandbare identical (same wallTime, logical, and nodeId)
Comparison order: wallTime first, then logical, then nodeId (lexicographic).
const a: HLCTimestamp = { wallTime: 1000, logical: 0, nodeId: 'node-a' }
const b: HLCTimestamp = { wallTime: 1000, logical: 1, nodeId: 'node-b' }
HybridLogicalClock.compare(a, b) // negative (a is before b, because a.logical < b.logical)generateUUIDv7()
Generates a UUID v7 identifier. UUID v7 values are time-sortable and contain a millisecond-precision timestamp, making them suitable for record IDs and node IDs.
Signature
function generateUUIDv7(): stringReturns
string -- A new UUID v7 string (e.g., '0190a6e0-7b3c-7def-8a12-4b5c6d7e8f90').
Example
import { generateUUIDv7 } from 'korajs'
const id = generateUUIDv7()createOperation()
Creates a new immutable, content-addressed operation. The operation's id is derived from a SHA-256 hash of its contents, ensuring that identical operations always produce the same ID.
Signature
function createOperation(input: OperationInput, clock: HybridLogicalClock): Promise<Operation>createOperation is asynchronous (the content-addressed id is derived via a SHA-256 digest) and takes the HLC as a second argument. The operation's timestamp is generated internally from the clock, so it is not part of OperationInput.
Parameters
| Parameter | Type | Description |
|---|---|---|
input | OperationInput | Operation data. See fields below. |
clock | HybridLogicalClock | The HLC used to generate this operation's timestamp. |
OperationInput
| Field | Type | Required | Description |
|---|---|---|---|
nodeId | string | Yes | UUID v7 of the originating device. |
type | 'insert' | 'update' | 'delete' | Yes | What kind of mutation this represents. |
collection | string | Yes | Target collection name (from schema). |
recordId | string | Yes | ID of the affected record. |
data | Record<string, unknown> | null | Yes | Field values. null for delete. For updates, only changed fields. |
previousData | Record<string, unknown> | null | Yes | Previous values of changed fields (enables 3-way merge). null for insert/delete. |
sequenceNumber | number | Yes | Monotonically increasing per node. |
causalDeps | string[] | Yes | Operation IDs this operation causally depends on. Pass [] if none. |
schemaVersion | number | Yes | Schema version at time of creation. |
atomicOps | Record<string, AtomicOp> | No | Atomic operation intents for fields in data (e.g., increment, max). |
transactionId | string | No | Groups this operation with others in an atomic transaction. Not part of the content hash. |
mutationName | string | No | Human-readable name for the mutation group. For DevTools display. |
Returns
Promise<Operation> -- Resolves to an immutable operation with a computed content-addressed id.
Example
import { createOperation, HybridLogicalClock, generateUUIDv7 } from 'korajs'
const nodeId = generateUUIDv7()
const clock = new HybridLogicalClock(nodeId)
const op = await createOperation({
nodeId,
type: 'insert',
collection: 'todos',
recordId: generateUUIDv7(),
data: { title: 'Ship Kora v1', completed: false },
previousData: null,
sequenceNumber: 1,
causalDeps: [],
schemaVersion: 1,
}, clock)
console.log(op.id) // SHA-256 content hashTIP
In typical application code, you never call createOperation directly. The Store creates operations automatically when you call insert(), update(), or delete() on a collection. This function is exposed for advanced use cases like custom transports or testing.
op (Atomic Field Operations)
The op helper creates atomic field operations that are resolved against the current value at write time, rather than setting an absolute value. This prevents lost updates when multiple devices modify the same field concurrently.
import { op } from '@korajs/core'
// or
import { op } from 'korajs'op.increment(amount)
Increments a numeric field by the given amount.
await app.products.update(id, { quantity: op.increment(1) })
await app.products.update(id, { quantity: op.increment(-3) }) // decrement by 3op.decrement(amount)
Decrements a numeric field by the given amount. Equivalent to op.increment(-amount).
await app.products.update(id, { quantity: op.decrement(5) })op.max(value)
Sets the field to the given value only if it is greater than the current value.
await app.players.update(id, { highScore: op.max(newScore) })op.min(value)
Sets the field to the given value only if it is less than the current value.
await app.auctions.update(id, { lowestBid: op.min(myBid) })op.append(item)
Appends an item to an array field.
await app.todos.update(id, { tags: op.append('urgent') })op.remove(item)
Removes an item from an array field by value.
await app.todos.update(id, { tags: op.remove('draft') })WARNING
Atomic operations are resolved locally before creating the operation. They do not provide distributed atomicity. Concurrent op.increment(1) calls from two devices both apply their deltas correctly because the operation stores the resolved value and the previous value, enabling 3-way merge.
buildScopeMap()
Builds a scope map from a schema definition and a set of scope values. Used internally by sync scoping but available for custom scope logic.
Signature
function buildScopeMap(
schema: SchemaDefinition,
scopeValues: Record<string, unknown>
): ScopeMapParameters
| Parameter | Type | Description |
|---|---|---|
schema | SchemaDefinition | The validated schema from defineSchema(). |
scopeValues | Record<string, unknown> | Flat key-value pairs used to populate scope filters. |
Returns
ScopeMap: a Record<string, Record<string, unknown>> mapping collection names to their scope filter objects.
Schema sync rules (partial sync DSL)
Declare which collections sync and which fields filter data using root-level sync rules in defineSchema():
const schema = defineSchema({
version: 1,
collections: {
todos: {
fields: {
title: t.string(),
userId: t.string(),
orgId: t.string(),
},
},
auditLog: {
fields: {
message: t.string(),
},
},
},
sync: {
todos: { where: { userId: true, orgId: true } },
},
})Each where entry binds a record field to a scope value key:
| Value | Meaning |
|---|---|
true | Bind field to a scope value with the same name (userId → scopeValues.userId) |
'otherKey' | Bind field to a different scope value key (ownerId: 'userId') |
When schema.sync is present, only collections listed in sync (or with legacy collection.scope) participate in sync. Other collections are omitted from the scope map (partial sync).
Use with buildScopeMap(schema, scopeValues) or createKoraAuthSync({ schema }) on the client, and resolveSessionScopes() on the server.
extractScopeValuesFromClaims()
Extracts flat scope values from JWT (or auth) claims using the scope field names declared on your schema collections.
Signature
function extractScopeValuesFromClaims(
schema: SchemaDefinition,
claims: Record<string, unknown>
): Record<string, unknown>
function collectSchemaScopeFields(schema: SchemaDefinition): string[]Resolution order
For each scope field declared on any collection:
- Top-level claim with the same name (e.g.
orgId) - Nested
claims.scope[field] - JWT
sub→userIdwhenuserIdis a scope field
Pass the result to buildScopeMap() or use createKoraAuthSync({ schema }) to apply automatically during sync handshake.
Example
import { extractScopeValuesFromClaims, buildScopeMap } from '@korajs/core'
const scopeValues = extractScopeValuesFromClaims(schema, {
sub: 'user-abc',
orgId: 'org-123',
})
const scopeMap = buildScopeMap(schema, scopeValues)
// { todos: { userId: 'user-abc', orgId: 'org-123' }, ... }migrate() / MigrationBuilder
Creates a fluent migration builder for defining schema migration steps programmatically. The builder is immutable: each method returns a new instance.
import { migrate, migrationStepsToSQL, t } from '@korajs/core'migrate()
Returns a new empty MigrationBuilder.
const migration = migrate()
.addField('todos', 'priority', t.enum(['low', 'medium', 'high']).default('medium'))
.removeField('todos', 'legacyFlag')
.renameField('todos', 'desc', 'description')
.addIndex('todos', 'priority')MigrationBuilder methods
| Method | Description |
|---|---|
.addField(collection, field, builder) | Add a new field to a collection. |
.removeField(collection, field) | Remove a field from a collection. |
.renameField(collection, from, to) | Rename a field. |
.addIndex(collection, field) | Add an index on a field. |
.removeIndex(collection, field) | Remove an index. |
.backfill(collection, transform) | Apply a transform function to all existing records. |
.build()
Returns a MigrationDefinition containing the ordered list of steps.
const definition = migration.build()
console.log(definition.steps) // Array of MigrationStep objectsmigrationStepsToSQL()
Converts migration steps into SQL statements.
function migrationStepsToSQL(steps: readonly MigrationStep[]): string[]const sql = migrationStepsToSQL(definition.steps)
// ['ALTER TABLE todos ADD COLUMN priority TEXT DEFAULT \'medium\' CHECK(...)']quoteIdent()
Quotes a SQL identifier (table or column name) for safe interpolation into generated SQL. It wraps the identifier in double quotes and doubles any embedded double quote, following the SQL standard. Both SQLite and PostgreSQL honor this form, so quoting lets a valid JavaScript identifier (camelCase, PascalCase, or a word that happens to be a SQL keyword like order or select) round-trip through generated DDL and queries without producing invalid or ambiguous SQL.
Signature
function quoteIdent(name: string): stringParameters
| Parameter | Type | Description |
|---|---|---|
name | string | The raw identifier (collection name, or field/column name). |
Returns
string -- The identifier wrapped in double quotes, with any embedded double quotes doubled, ready to splice into SQL.
Example
import { quoteIdent } from '@korajs/core'
quoteIdent('formResponses') // '"formResponses"'
quoteIdent('order') // '"order"' (safe even though `order` is a SQL keyword)Blobs (content-addressed binary)
Blob helpers create and validate content-addressed references to binary data stored out of band. A blob field never carries the bytes in the operation log; it carries a small BlobRef keyed by the content hash, so identical content is stored once and never re-synced to a peer that already has that hash.
import { createBlobRef, hashBlob, isBlobRef } from '@korajs/core'hashBlob()
Computes the hex-encoded SHA-256 content hash of a byte buffer. This is the content address of a blob: identical bytes always hash to the same value.
function hashBlob(bytes: Uint8Array): Promise<string>createBlobRef()
Creates a content-addressed BlobRef for a byte buffer, carrying the content hash, size, and optional metadata.
function createBlobRef(bytes: Uint8Array, metadata?: BlobRefMetadata): Promise<BlobRef>| Parameter | Type | Description |
|---|---|---|
bytes | Uint8Array | The binary content. |
metadata | BlobRefMetadata | Optional MIME type and filename. Defaults to {}. |
isBlobRef()
Type guard that checks whether a value is a structurally valid BlobRef. It validates the shape only (a non-empty hex hash and a non-negative integer size); it does not verify that the bytes behind the hash exist or match.
function isBlobRef(value: unknown): value is BlobRefBlobRef
A content-addressed reference to binary data stored out of band.
interface BlobRef {
/** Hex-encoded SHA-256 hash of the bytes. The content address. */
hash: string
/** Size of the bytes in bytes. */
size: number
/** Optional MIME type (for example "image/png"). */
mimeType?: string
/** Optional original filename. */
filename?: string
/** Hex-encoded SHA-256 hash of this blob's manifest (the chunk index a peer fetches first). Present when the blob was stored for transfer; absent for a bare reference. */
manifestHash?: string
}BlobRefMetadata
Optional metadata carried alongside a blob reference.
interface BlobRefMetadata {
mimeType?: string
filename?: string
}Types
Operation
The atomic unit of mutation in Kora.js. Every data change produces an Operation. Operations are immutable and content-addressed.
interface Operation {
/** SHA-256 hash of content. Content-addressed. */
id: string
/** UUID v7 of the originating device. */
nodeId: string
/** What happened. */
type: 'insert' | 'update' | 'delete'
/** Which collection (from schema). */
collection: string
/** ID of the affected record. */
recordId: string
/** Field values. null for delete. For updates, only changed fields. */
data: Record<string, unknown> | null
/** Previous values of changed fields (enables 3-way merge). null for insert/delete. */
previousData: Record<string, unknown> | null
/** Hybrid Logical Clock timestamp. */
timestamp: HLCTimestamp
/** Monotonically increasing per node. Used in version vectors. */
sequenceNumber: number
/** Operation IDs this operation causally depends on (direct parents in the DAG). */
causalDeps: string[]
/** Schema version at time of creation. */
schemaVersion: number
/** Atomic operation intents for fields in data (e.g., increment, max). Present only when atomic ops were used. */
atomicOps?: Record<string, AtomicOp>
/** Groups this operation with others in an atomic transaction. Not part of the content hash. */
transactionId?: string
/** Human-readable name for the mutation group (e.g., 'complete-sale'). For DevTools display. */
mutationName?: string
}HLCTimestamp
A timestamp produced by the Hybrid Logical Clock. Provides total ordering across distributed devices.
interface HLCTimestamp {
/** Physical wall-clock time in milliseconds since epoch. */
wallTime: number
/** Logical counter. Increments when wallTime has not changed since last event. */
logical: number
/** Node ID for tie-breaking. Ensures total order even with identical wall + logical. */
nodeId: string
}VersionVector
Tracks the latest sequence number seen from each node. Used for delta sync computation.
type VersionVector = Map<string, number> // nodeId -> max sequence numberSchemaDefinition
The validated output of defineSchema(). Passed to createApp().
interface SchemaDefinition {
version: number
collections: Record<string, CollectionDefinition>
relations: Record<string, RelationDefinition>
/** Schema migrations keyed by target version. */
migrations: Record<number, MigrationDefinition>
/** Declarative partial-sync rules per collection. Present only when sync rules are declared. */
sync?: Record<string, SyncRuleDefinition>
}FieldDescriptor
Describes a single field's type, default value, and modifiers. Produced by the t type builders.
interface FieldDescriptor {
kind: FieldKind
required: boolean
defaultValue: unknown
auto: boolean
enumValues: readonly string[] | null
itemKind: FieldKind | null // Element kind for array fields
mergeStrategy: FieldMergeStrategy | null
transitions: TransitionMap | null // State machine transitions for enum fields
nestedFields?: Record<string, FieldDescriptor> | null // For object fields
secretMode?: SecretMode | null // For secret fields
}FieldKind is one of 'string' | 'number' | 'boolean' | 'timestamp' | 'richtext' | 'enum' | 'array' | 'object' | 'json' | 'blob' | 'secret'. FieldMergeStrategy is one of 'lww' | 'counter' | 'max' | 'min' | 'union' | 'append-only' | 'server-authoritative'. SecretMode is 'hashed' | 'encrypted'.
MergeTrace
Records the full context of a merge decision. Used by DevTools for conflict inspection.
interface MergeTrace {
/** First concurrent operation. */
operationA: Operation
/** Second concurrent operation. */
operationB: Operation
/** The field where the conflict occurred. */
field: string
/** Which strategy resolved the conflict (e.g. 'lww', 'crdt-text', 'add-wins-set', 'unique-constraint', 'custom'). */
strategy: string
/** Value from operation A. */
inputA: unknown
/** Value from operation B. */
inputB: unknown
/** Base value (before either operation). null if unavailable. */
base: unknown | null
/** The resolved output value. */
output: unknown
/** Which tier resolved this conflict. */
tier: 1 | 2 | 3
/** Name of the violated constraint, or null if no constraint was involved. */
constraintViolated: string | null
/** Time spent resolving in milliseconds. */
duration: number
}KoraEvent
Union type of all instrumentation events emitted by the Kora runtime. Consumed by DevTools and custom event handlers.
type KoraEvent =
| { type: 'operation:created'; operation: Operation }
| { type: 'operation:applied'; operation: Operation; duration: number }
| { type: 'merge:started'; operationA: Operation; operationB: Operation }
| { type: 'merge:completed'; trace: MergeTrace }
| { type: 'merge:conflict'; trace: MergeTrace }
| { type: 'constraint:violated'; constraint: string; trace: MergeTrace }
| { type: 'sync:connected'; nodeId: string }
| { type: 'sync:disconnected'; reason: string }
| {
type: 'sync:schema-mismatch'
clientSchemaVersion: number
serverSchemaVersion: number
supportedMin: number
supportedMax: number
reason: string
}
| { type: 'sync:auth-failed'; reason: string }
| {
type: 'sync:clock-skew'
/** serverTime - localTime in ms. Negative = this device's clock is fast. */
skewMs: number
severity: 'info' | 'slow-warning' | 'fast-blocked'
source: 'handshake' | 'server-reject'
}
| {
type: 'sync:clock-rebase'
/** Number of unsynced operations that were re-stamped. */
rebasedCount: number
/** How far ahead of server time the most future queued operation was, in ms. */
maxSkewMs: number
}
| { type: 'sync:sent'; operations: Operation[]; batchSize: number }
| { type: 'sync:received'; operations: Operation[]; batchSize: number }
| { type: 'sync:acknowledged'; sequenceNumber: number }
| {
type: 'sync:apply-failed'
operationId: string
collection: string
recordId: string
code: string
message: string
retriable: boolean
}
| {
type: 'sync:operation-rejected'
operationId: string
collection: string
recordId: string
code: string
message: string
retriable: boolean
}
| { type: 'query:subscribed'; queryId: string; collection: string }
| { type: 'query:invalidated'; queryId: string; trigger: Operation }
| { type: 'query:executed'; queryId: string; duration: number; resultCount: number }
| { type: 'connection:quality'; quality: ConnectionQuality }
| { type: 'sync:diagnostics'; diagnostics: SyncDiagnosticsSnapshot }
| { type: 'sync:bandwidth'; bytesPerSecond: number; direction: 'in' | 'out' }
| {
type: 'sync:initial-sync-progress'
progress: number
totalBatches: number
receivedBatches: number
}
| { type: 'awareness:updated'; states: Map<number, unknown> }
| {
type: 'state-machine:transition'
collection: string
recordId: string
from: string
to: string
valid: boolean
}
| {
type: 'state-machine:rejected'
collection: string
recordId: string
from: string
to: string
allowed: string[]
}
| { type: 'store:persistence-error'; dbName: string; message: string; code: string }
| { type: 'store:quota-exceeded'; dbName: string; message: string }
| {
type: 'store:opfs-unavailable'
dbName: string
reason: 'lock-conflict' | 'timeout' | 'unsupported'
message: string
}
| {
type: 'store:storage-fallback'
dbName: string
from: 'opfs' | 'sqlite-wasm'
to: 'indexeddb'
reason: 'lock-conflict' | 'timeout' | 'unsupported'
message: string
}
| { type: 'store:db-name-collision'; dbName: string; message: string }
| {
type: 'replay:completed'
targetOperationId: string
operationsApplied: number
duration: number
}WARNING
operation:applied is declared in the catalog but is not yet emitted by the store. Do not rely on it firing; subscribe to operation:created to observe writes.
KoraError
Base error class for all Kora.js errors. Includes a machine-readable code and optional context for debugging.
Constructor
new KoraError(message: string, code: string, context?: Record<string, unknown>)| Parameter | Type | Description |
|---|---|---|
message | string | Human-readable error message. |
code | string | Machine-readable error code (e.g., 'INVALID_SCHEMA', 'MERGE_CONFLICT'). |
context | Record<string, unknown> | Optional. Additional data for debugging. |
Properties
| Property | Type | Description |
|---|---|---|
message | string | Human-readable error message. |
code | string | Machine-readable error code. |
context | Record<string, unknown> | undefined | Additional debugging data. |
name | string | Always 'KoraError'. |
Error codes
| Code | Description |
|---|---|
INVALID_SCHEMA | Schema definition is malformed or contains invalid references. |
MERGE_CONFLICT | A merge conflict could not be resolved automatically. |
CONSTRAINT_VIOLATION | A constraint was violated and the onConflict strategy failed. |
STORAGE_ERROR | A storage adapter operation failed. |
SYNC_ERROR | A sync protocol error occurred. |
CLOCK_DRIFT | The local clock has drifted more than 5 minutes behind the HLC. |
INVALID_OPERATION | An operation failed validation. |
Example
import { KoraError } from 'korajs'
try {
await app.todos.insert({ title: 123 }) // wrong type
} catch (err) {
if (err instanceof KoraError) {
console.error(err.code) // 'INVALID_OPERATION'
console.error(err.context) // { field: 'title', expected: 'string', received: 'number' }
}
}State Machine Constraints
State machine constraints enforce valid transitions on enum fields. When a state machine is declared on an enum field, mutations and merges verify that the field only moves along allowed transitions. This prevents invalid state changes such as moving an order directly from 'draft' to 'shipped'.
import { validateTransition, buildStateMachineConstraints, getTransitionMap } from '@korajs/core'.transitions() on EnumFieldBuilder
The .transitions() method is available on t.enum() fields. It accepts a map of source states to allowed target states and returns a new EnumFieldBuilder with the transition rules attached.
t.enum(['draft', 'pending', 'confirmed', 'cancelled']).transitions({
draft: ['pending', 'cancelled'],
pending: ['confirmed', 'cancelled'],
confirmed: [],
cancelled: [],
})Both the source and target states in the map must be valid enum values. The method throws a SchemaValidationError if any state in the map is not one of the declared enum values.
Full schema example
import { defineSchema, t } from 'korajs'
const schema = defineSchema({
version: 1,
collections: {
orders: {
fields: {
title: t.string(),
status: t.enum(['draft', 'submitted', 'approved', 'cancelled'])
.default('draft')
.transitions({
draft: ['submitted', 'cancelled'],
submitted: ['approved', 'cancelled'],
approved: [],
cancelled: [],
}),
},
},
},
})validateTransition()
Validates whether a transition from one state to another is allowed by a given state machine constraint.
Signature
function validateTransition(
constraint: StateMachineConstraint,
fromValue: unknown,
toValue: unknown
): TransitionValidationResultParameters
| Parameter | Type | Description |
|---|---|---|
constraint | StateMachineConstraint | The state machine constraint defining allowed transitions. |
fromValue | unknown | The current state value (before the transition). Coerced to string. |
toValue | unknown | The target state value (after the transition). Coerced to string. |
Returns
TransitionValidationResult -- An object describing whether the transition is valid, along with the source state, target state, field name, collection name, and the full list of allowed targets from the source state.
Example
import { validateTransition } from '@korajs/core'
const constraint = {
field: 'status',
collection: 'orders',
transitions: {
draft: ['submitted', 'cancelled'],
submitted: ['approved'],
approved: [],
cancelled: [],
},
}
const result = validateTransition(constraint, 'draft', 'submitted')
// { valid: true, from: 'draft', to: 'submitted', field: 'status',
// collection: 'orders', allowedTargets: ['submitted', 'cancelled'] }
const invalid = validateTransition(constraint, 'draft', 'approved')
// { valid: false, from: 'draft', to: 'approved', field: 'status',
// collection: 'orders', allowedTargets: ['submitted', 'cancelled'] }buildStateMachineConstraints()
Extracts all state machine constraints from a schema definition. Scans every collection for enum fields that have transition rules declared via the .transitions() builder method.
Signature
function buildStateMachineConstraints(schema: SchemaDefinition): StateMachineConstraint[]Parameters
| Parameter | Type | Description |
|---|---|---|
schema | SchemaDefinition | The validated schema from defineSchema(). |
Returns
StateMachineConstraint[] -- An array of constraint objects, one per enum field with transitions declared. Returns an empty array if no fields have transitions.
Example
import { defineSchema, t, buildStateMachineConstraints } from '@korajs/core'
const schema = defineSchema({
version: 1,
collections: {
orders: {
fields: {
status: t.enum(['draft', 'submitted']).transitions({
draft: ['submitted'],
submitted: [],
}),
title: t.string(),
},
},
},
})
const constraints = buildStateMachineConstraints(schema)
// [{ field: 'status', collection: 'orders', transitions: { draft: ['submitted'], submitted: [] } }]getTransitionMap()
Finds the transition map for a specific field in a specific collection, if one exists.
Signature
function getTransitionMap(
schema: SchemaDefinition,
collection: string,
field: string
): TransitionMap | nullParameters
| Parameter | Type | Description |
|---|---|---|
schema | SchemaDefinition | The validated schema from defineSchema(). |
collection | string | The collection name to search. |
field | string | The field name to look up. |
Returns
TransitionMap | null -- The transition map if the field has transitions declared, or null if the collection does not exist, the field does not exist, the field is not an enum, or no transitions are declared.
Example
const transitions = getTransitionMap(schema, 'orders', 'status')
// { draft: ['submitted'], submitted: [] }
const none = getTransitionMap(schema, 'orders', 'title')
// null (title is a string field, not an enum with transitions)State machine types
StateMachineConstraint
A state machine constraint extracted from the schema. Used by merge and validation to enforce valid state transitions.
interface StateMachineConstraint {
/** The enum field this constraint controls */
field: string
/** The collection this constraint applies to */
collection: string
/** Map of state to allowed next states */
transitions: TransitionMap
}TransitionMap
Map of state names to allowed next states.
type TransitionMap = Record<string, string[]>TransitionValidationResult
Result of validating a state transition.
interface TransitionValidationResult {
/** Whether the transition is allowed */
valid: boolean
/** The source state */
from: string
/** The target state */
to: string
/** The field being transitioned */
field: string
/** The collection containing the field */
collection: string
/** All allowed target states from the source state */
allowedTargets: string[]
}StateMachineDefinition
Defines a state machine on an enum field at the collection level, constraining valid state transitions. Used as the stateMachine property in a CollectionDefinition.
interface StateMachineDefinition {
/** The enum field this state machine controls */
field: string
/** Map of state to allowed next states */
transitions: Record<string, string[]>
/** What to do when an invalid transition is attempted */
onInvalidTransition: 'reject' | 'last-valid-state'
}onInvalidTransition value | Behavior |
|---|---|
'reject' | The mutation is rejected and an error is thrown. |
'last-valid-state' | The field retains its previous value instead of transitioning. |
Migration Rollbacks
Migration rollbacks allow you to reverse schema migrations, either automatically (when the inverse is deterministic) or via explicit rollback steps. This builds on top of the migrate() / MigrationBuilder API documented above.
import { canAutoRollback, generateRollbackSteps, createReversibleMigration } from '@korajs/core'canAutoRollback()
Determines whether a single forward migration step can be automatically rolled back without explicit developer-provided down steps.
Signature
function canAutoRollback(step: MigrationStep): booleanParameters
| Parameter | Type | Description |
|---|---|---|
step | MigrationStep | The forward migration step to check. |
Returns
boolean -- true if the step can be auto-rolled back, false if it requires an explicit .down() definition.
Auto-rollback support by step type
| Step type | Auto-rollback | Inverse operation |
|---|---|---|
addField | Yes | removeField (drops the added column) |
addIndex | Yes | removeIndex (drops the added index) |
removeIndex | Yes | addIndex (re-creates the index) |
renameField | Yes | renameField (swaps from/to names) |
removeField | No | Requires the field descriptor to re-create the column. Provide a FieldBuilder to removeField() or use .down(). |
backfill | No | Data transforms are not reversible. Provide a reverseTransform on the step or use .down(). |
Example
import { canAutoRollback } from '@korajs/core'
canAutoRollback({ type: 'addField', collection: 'todos', field: 'priority', descriptor: /* ... */ })
// true
canAutoRollback({ type: 'backfill', collection: 'todos', transform: (r) => r })
// falsegenerateRollbackSteps()
Generates rollback steps for a list of forward migration steps. Steps are reversed in order: the last forward step becomes the first rollback step.
Signature
function generateRollbackSteps(forwardSteps: readonly MigrationStep[]): MigrationStep[]Parameters
| Parameter | Type | Description |
|---|---|---|
forwardSteps | readonly MigrationStep[] | The forward migration steps to generate rollbacks for. |
Returns
MigrationStep[] -- Array of rollback steps in reverse execution order.
Errors
Throws MigrationRollbackError if any step cannot be auto-rolled back. Use canAutoRollback() to check before calling, or provide explicit down steps via the MigrationBuilder .down() API instead.
Example
import { migrate, generateRollbackSteps } from '@korajs/core'
const migration = migrate()
.addField('todos', 'priority', t.enum(['low', 'medium', 'high']).default('medium'))
.addIndex('todos', 'priority')
const rollbackSteps = generateRollbackSteps(migration.steps)
// [
// { type: 'removeIndex', collection: 'todos', field: 'priority' },
// { type: 'removeField', collection: 'todos', field: 'priority' },
// ]createReversibleMigration()
Creates a ReversibleMigration from forward steps, optional explicit down steps, and version information. If explicit down steps are provided, they are used as-is. Otherwise, auto-generation is attempted via generateRollbackSteps().
Signature
function createReversibleMigration(
upSteps: readonly MigrationStep[],
downSteps: readonly MigrationStep[] | null,
fromVersion: number,
toVersion: number
): ReversibleMigrationParameters
| Parameter | Type | Description |
|---|---|---|
upSteps | readonly MigrationStep[] | The forward migration steps. |
downSteps | readonly MigrationStep[] | null | Optional explicit rollback steps. Pass null to auto-generate. |
fromVersion | number | The schema version before the migration. |
toVersion | number | The schema version after the migration. |
Returns
ReversibleMigration -- A complete reversible migration with both up and down steps.
Errors
Throws MigrationRollbackError if downSteps is null and auto-generation fails for any step.
Example
import { migrate, createReversibleMigration, t } from '@korajs/core'
const migration = migrate()
.addField('todos', 'priority', t.enum(['low', 'medium', 'high']).default('medium'))
.addIndex('todos', 'priority')
// Auto-generated rollback
const reversible = createReversibleMigration(migration.steps, null, 1, 2)
console.log(reversible.fromVersion) // 1
console.log(reversible.toVersion) // 2
console.log(reversible.down)
// [
// { type: 'removeIndex', collection: 'todos', field: 'priority' },
// { type: 'removeField', collection: 'todos', field: 'priority' },
// ]MigrationBuilder .down()
The .down() method on MigrationBuilder lets you define explicit rollback steps. This is required when a migration contains steps that cannot be auto-rolled back (such as removeField without a descriptor, or backfill without a reverseTransform).
Signature
down(fn: (rollback: RollbackBuilder) => void): MigrationBuilderThe RollbackBuilder passed to the callback provides the same step methods as MigrationBuilder: addField(), removeField(), renameField(), addIndex(), removeIndex(), and backfill().
Example
import { migrate, t } from '@korajs/core'
const migration = migrate()
.removeField('todos', 'legacyFlag')
.backfill('todos', (record) => ({
priority: record.urgency === 'high' ? 'high' : 'medium',
}))
.down((rollback) => {
rollback
.addField('todos', 'legacyFlag', t.boolean().default(false))
.backfill('todos', (record) => ({
urgency: record.priority === 'high' ? 'high' : 'normal',
}))
})
console.log(migration.safelyReversible) // trueMigrationRollbackError
Error thrown when a migration step cannot be automatically rolled back and no explicit down step has been provided. Extends KoraError with code 'MIGRATION_ROLLBACK'.
class MigrationRollbackError extends KoraError {
constructor(step: MigrationStep)
}The error message includes the step type and collection name, telling you exactly which step needs an explicit .down() definition.
ReversibleMigration
A migration that includes both forward (up) and backward (down) steps, along with version metadata.
interface ReversibleMigration {
readonly up: readonly MigrationStep[]
readonly down: readonly MigrationStep[]
readonly fromVersion: number
readonly toVersion: number
}Protobuf Code Generation
Generates Protocol Buffer definitions from a Kora schema. Useful for producing .proto files for external tooling, type-safe binary serialization, or runtime protobufjs usage without parsing .proto text.
import { generateProtoDefinitions } from '@korajs/core'generateProtoDefinitions()
Converts a validated schema into Protocol Buffer definitions. Produces three outputs: the .proto file text, a type map linking Kora field paths to protobuf types, and a JSON descriptor compatible with protobufjs Root.fromJSON().
Signature
function generateProtoDefinitions(schema: SchemaDefinition): ProtoOutputParameters
| Parameter | Type | Description |
|---|---|---|
schema | SchemaDefinition | A validated schema from defineSchema(). |
Returns
ProtoOutput -- An object containing:
| Field | Type | Description |
|---|---|---|
proto | string | The generated .proto file content as a string (proto3 syntax). |
typeMap | Map<string, string> | Maps Kora field paths ("collection.field") to protobuf type strings. |
jsonDescriptor | Record<string, unknown> | JSON descriptor for runtime protobufjs usage via Root.fromJSON(). |
Generated messages
The output includes the following protobuf messages:
| Message | Description |
|---|---|
{Collection}Record | Per-collection record message (e.g., TodosRecord for a todos collection). Includes an id field and all schema-defined fields. |
KoraOperation | Wire format for individual operations in the sync protocol. |
OperationBatch | Batches operations for sync transfer with an is_final flag. |
HandshakeMessage | Initiates a sync session with version vector and schema version. |
HandshakeResponse | Server acknowledges with its own version vector. |
Acknowledgment | Confirms receipt of an operation batch. |
Type mapping
Kora field kinds are mapped to protobuf scalar types as follows:
| Kora field kind | Protobuf type |
|---|---|
string | string |
number | double |
boolean | bool |
timestamp | int64 |
richtext | bytes |
enum | Generated nested enum (e.g., TodosRecordPriority) |
array | repeated of the item's scalar type |
Enum fields produce a nested protobuf enum inside the parent message. A sentinel _UNSPECIFIED = 0 value is always added as the first entry, following proto3 conventions.
Collection names are converted to PascalCase for message names (e.g., todo_items becomes TodoItemsRecord). Field names are converted to snake_case for protobuf field names (e.g., dueDate becomes due_date).
Example
import { defineSchema, t, generateProtoDefinitions } from '@korajs/core'
const schema = defineSchema({
version: 1,
collections: {
todos: {
fields: {
title: t.string(),
completed: t.boolean().default(false),
priority: t.enum(['low', 'medium', 'high']).default('medium'),
tags: t.array(t.string()).default([]),
},
},
},
})
const { proto, typeMap, jsonDescriptor } = generateProtoDefinitions(schema)The proto string for this schema produces:
syntax = "proto3";
package kora;
// Collection record messages
message TodosRecord {
string id = 1;
string title = 2;
bool completed = 3;
TodosRecordPriority priority = 4;
repeated string tags = 5;
enum TodosRecordPriority {
TODOSRECORDPRIORITY_UNSPECIFIED = 0;
TODOSRECORDPRIORITY_LOW = 1;
TODOSRECORDPRIORITY_MEDIUM = 2;
TODOSRECORDPRIORITY_HIGH = 3;
}
}
// Sync protocol messages
message KoraOperation { ... }
message OperationBatch { ... }
message HandshakeMessage { ... }
message HandshakeResponse { ... }
message Acknowledgment { ... }The typeMap contains:
typeMap.get('todos.id') // 'string'
typeMap.get('todos.title') // 'string'
typeMap.get('todos.completed') // 'bool'
typeMap.get('todos.priority') // 'TodosRecordPriority'
typeMap.get('todos.tags') // 'repeated string'The jsonDescriptor can be loaded directly with protobufjs for runtime encoding and decoding:
import protobuf from 'protobufjs'
const root = protobuf.Root.fromJSON(jsonDescriptor)
const TodosRecord = root.lookupType('kora.TodosRecord')Store identity
app.storeInfo() returns the base name, resolved database name, auth user id, persistence adapter, durability, and isolation lifecycle state without exposing another namespace's data.

