Schemas
Schemas define which fields get generated columns for fast querying. Fields not in the schema are still stored - they just live in the JSONB blob.
How Storage Works
Every document is stored as JSONB. When you define a field in your schema, StrataDB creates a generated column that extracts that value for efficient querying:
type User = Document<{
email: string // Will have generated column (defined in schema)
password: string // Stored in JSONB only (not in schema)
profile: { // Stored in JSONB only
bio: string
avatar: string
}
}>
const schema = createSchema<User>()
.field('email', { type: 'TEXT', indexed: true, unique: true })
.build()In this example:
email→ Generated column + index (fast lookups, unique constraint)password,profile→ Stored in JSONB blob (no generated column)
You can still store and retrieve password and profile - they're part of your document. You just can't efficiently query them with operators like $eq or $gt.
Generated Columns vs Indexes
These are separate concepts:
| Option | What it does | Use case |
|---|---|---|
.field('x', { type: 'TEXT' }) | Creates generated column only | Extracting for unique constraints, sorting |
.field('x', { type: 'TEXT', indexed: true }) | Generated column + B-tree index | Fast queries on this field |
.field('x', { type: 'TEXT', unique: true }) | Generated column + unique constraint | Enforce uniqueness (no index) |
.field('x', { type: 'TEXT', indexed: true, unique: true }) | All three | Fast unique lookups |
Why not auto-index every field?
Indexes have costs: write overhead and storage. You might define a field just for unique constraints or computed sorting without needing query performance. StrataDB keeps these concerns separate so you choose explicitly.
Creating a Schema
Use createSchema<T>() for a fluent builder API:
import { createSchema, type Document } from 'stratadb'
type User = Document<{
name: string
email: string
age: number
}>
const userSchema = createSchema<User>()
.field('name', { type: 'TEXT', indexed: true })
.field('email', { type: 'TEXT', indexed: true, unique: true })
.field('age', { type: 'INTEGER', indexed: true })
.build()Field Options
.field('fieldName', {
type: 'TEXT', // SQLite type (required)
indexed: true, // Create index for fast queries
unique: true, // Enforce uniqueness
nullable: false, // Disallow null values
default: 'value', // Default value on insert
path: '$.nested.field' // JSON path for nested fields
})SQLite Types
| Type | TypeScript | Description |
|---|---|---|
TEXT | string | Text strings |
INTEGER | number | Integers |
REAL | number | Floating point |
BLOB | Uint8Array | Binary data |
BOOLEAN | boolean | Stored as INTEGER (0/1) |
NUMERIC | number | Numeric affinity |
Indexes
Indexed fields are extracted into generated columns for fast B-tree lookups:
// Indexed field - uses generated column, O(log n) queries
.field('email', { type: 'TEXT', indexed: true })
// Non-indexed field - uses jsonb_extract, O(n) queries
.field('bio', { type: 'TEXT' })Compound Indexes
For queries on multiple fields:
const schema = createSchema<User>()
.field('age', { type: 'INTEGER', indexed: true })
.field('status', { type: 'TEXT', indexed: true })
.compoundIndex('age_status', ['age', 'status'])
.build()
// This query uses the compound index efficiently:
await users.find({ age: { $gte: 18 }, status: 'active' })Unique Constraints
// Single field unique
.field('email', { type: 'TEXT', indexed: true, unique: true })
// Compound unique (e.g., one email per tenant)
.compoundIndex('email_tenant', ['email', 'tenantId'], { unique: true })Timestamps
Enable automatic timestamp management:
const schema = createSchema<User>()
.field('name', { type: 'TEXT' })
.timestamps(true) // Enable createdAt/updatedAt
.build()Validation
Add runtime validation using Zod, ArkType, or custom functions:
import { z } from 'zod'
const UserValidator = z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().int().min(0),
})
const schema = createSchema<User>()
.field('name', { type: 'TEXT' })
.field('email', { type: 'TEXT', unique: true })
.field('age', { type: 'INTEGER' })
.validate((doc): doc is User => UserValidator.safeParse(doc).success)
.build()See Validation for more examples.
Inline Schema Definition
You can also define schemas inline when creating collections:
const users = db.collection<User>('users')
.field('name', { type: 'TEXT', indexed: true })
.field('email', { type: 'TEXT', indexed: true, unique: true })
.build()