Models & Schema Definition
Pulsabase offers a two-way schema sync approach to schema management. Define your schema in TypeScript using decorators, or build it visually in the Dashboard and pull it as typed models.
Two-Way Schema Sync
Section titled “Two-Way Schema Sync”The CLI provides complete bidirectional sync:
- Code → Database (
db:sync): You define models in TypeScript, and Pulsabase generates and applies SQL migrations to update your remote database. - Database → Code (
db:pull): You create tables in the visual Dashboard, and Pulsabase generates the perfectly typed TypeScript models locally in your project.
Defining a Model
Section titled “Defining a Model”A model is a TypeScript class with decorated properties (columns) and a static schema block for table-level configuration.
import { column, primary, text, unique, boolean, integer, timestamp, ModelSchema, rules, crossRules } from 'pulsabase';
export default class User { @primary // UUID primary key (auto-generated) id: string;
@text(false) // Non-nullable TEXT column name: string;
@text(false) @unique() // Adds a UNIQUE constraint email: string;
@integer() age?: number;
@boolean() is_active?: boolean;
// The `schema` block defines table-level features. // Only the TypeScript compiler needs this — it's not sent to the DB directly. static schema: ModelSchema = { tableName: 'users', publication: true, // Enable real-time subscriptions for this table timestamps: true, // Auto-manage `created_at` / `updated_at`
indexes: [ { columns: ['email'], unique: true }, { columns: ['age'] }, ],
validation: { email: [rules.required(), rules.email('Must be a valid email')], name: [rules.required(), rules.min(3, 'Name is too short')], age: [rules.positive('Age must be positive')], },
crossValidation: [ crossRules.dateAfter('ends_at', 'starts_at', 'End must be after start'), ],
policies: [ { action: ['SELECT', 'UPDATE'], authenticated: true, owner: 'id' }, { action: ['INSERT'], authenticated: true }, ], };}import 'package:pulsabase/pulsabase.dart';
@PulsaModel(tableName: 'users')class User extends PulsaModelBase {
@PulsaPrimary() @PulsaFields.uuid() String? id;
@PulsaFields.text(nullable: false) String? name;
@PulsaFields.text(unique: true, nullable: false) String? email;
@PulsaFields.integer() int? age;
@PulsaFields.timestamp() DateTime? createdAt;
// Required: fromJson / toJson User.fromJson(Map<String, dynamic> json) { id = json['id']; name = json['name']; email = json['email']; age = json['age']; createdAt = json['created_at'] != null ? DateTime.parse(json['created_at']) : null; }
Map<String, dynamic> toJson() => { 'id': id, 'name': name, 'email': email, 'age': age, 'created_at': createdAt?.toIso8601String(), };}Column Decorators
Section titled “Column Decorators”| Decorator | SQL Type | Notes |
|---|---|---|
@primary | UUID DEFAULT gen_random_uuid() | Primary key. Auto-generated. |
@text(nullable?) | TEXT | Default: nullable. false → NOT NULL |
@integer() | INTEGER | |
@bigInteger() | BIGINT | For large numbers |
@float() | DOUBLE PRECISION | |
@boolean() | BOOLEAN | |
@timestamp() | TIMESTAMPTZ | Timezone-aware timestamp |
@json() | JSONB | Queryable JSON (PostgreSQL JSONB) |
@uuid() | UUID | For FK references |
@column(type) | custom | Raw SQL type override |
@unique() | — | Adds a UNIQUE constraint |
@default(value) | — | Sets a SQL DEFAULT expression |
Schema Block Properties
Section titled “Schema Block Properties”| Property | Type | Description |
|---|---|---|
tableName | string | Required. The PostgreSQL table name. |
timestamps | boolean | Auto-add and manage created_at / updated_at columns. |
publication | boolean | Register the table for real-time change events. |
indexes | array | Define custom indexes (see below). |
validation | object | Server-side validation rules per field. |
crossValidation | array | Cross-field validation rules. |
policies | array | RLS policies (see Row-Level Security). |
Validation Rules
Section titled “Validation Rules”Validation runs server-side before data reaches the database. It provides structured error responses that are ideal for displaying form errors.
import { rules, crossRules } from 'pulsabase';
static schema: ModelSchema = { tableName: 'events', validation: { title: [rules.required(), rules.min(5, 'Title must be at least 5 chars')], email: [rules.email('Invalid email format')], ticket_price: [rules.positive('Price must be > 0')], phone: [rules.regex(/^\+?[0-9]{10,14}$/, 'Invalid phone number')], }, crossValidation: [ crossRules.dateAfter('end_date', 'start_date', 'End date must be after start date'), ],};Indexes
Section titled “Indexes”static schema: ModelSchema = { tableName: 'documents', indexes: [ { columns: ['user_id'] }, // Standard BTREE { columns: ['email', 'project_id'], unique: true }, // Composite unique { columns: ['created_at'] }, // Timestamp index { columns: ['embedding'], type: 'HNSW', opclass: 'vector_l2_ops' } // pgvector ],};Syncing the Schema
Section titled “Syncing the Schema”# Preview what changes will be generated (no modifications applied)pulsabase db:sync --dry-run
# Apply the schema to the databasepulsabase db:sync
# Pull the database schema as local model filespulsabase db:pullSee CLI Reference for all available options.