Skip to content
On this page

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:

typescript
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:

OptionWhat it doesUse case
.field('x', { type: 'TEXT' })Creates generated column onlyExtracting for unique constraints, sorting
.field('x', { type: 'TEXT', indexed: true })Generated column + B-tree indexFast queries on this field
.field('x', { type: 'TEXT', unique: true })Generated column + unique constraintEnforce uniqueness (no index)
.field('x', { type: 'TEXT', indexed: true, unique: true })All threeFast 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:

typescript
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

typescript
.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

TypeTypeScriptDescription
TEXTstringText strings
INTEGERnumberIntegers
REALnumberFloating point
BLOBUint8ArrayBinary data
BOOLEANbooleanStored as INTEGER (0/1)
NUMERICnumberNumeric affinity

Indexes

Indexed fields are extracted into generated columns for fast B-tree lookups:

typescript
// 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:

typescript
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

typescript
// 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:

typescript
const schema = createSchema<User>()
  .field('name', { type: 'TEXT' })
  .timestamps(true)  // Enable createdAt/updatedAt
  .build()

Validation

Add runtime validation using Zod, ArkType, or custom functions:

typescript
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:

typescript
const users = db.collection<User>('users')
  .field('name', { type: 'TEXT', indexed: true })
  .field('email', { type: 'TEXT', indexed: true, unique: true })
  .build()

Released under the MIT License.