PostgreSQL Overview
PostgreSQL adapter for persistent storage in JustScale
The PostgreSQL adapter provides a production-ready storage backend for JustScale applications. It wraps domain models with PostgreSQL-specific configuration, separating domain concerns from storage implementation details.
Installation
Install the PostgreSQL adapter package:
pnpm add @justscale/postgresCore Concepts
Domain Models vs Storage Models
JustScale separates domain models (pure business logic) from storage models (database-specific configuration):
- Domain Model - Defined with
defineModel, contains field types and validation, no storage details - Storage Model - Created with
createPgModel, wraps the domain model with table name, indexes, column overrides - Repository - Created with
createPgRepository, provides query methods and integrates with DI
Quick Start
Here is a complete example showing all three layers:
import { defineModel, field } from '@justscale/models';
import { createPgModel, createPgRepository } from '@justscale/postgres';
// 1. Define domain model (pure, no storage details)
class User extends defineModel({
email: field.string().max(255).unique(),
name: field.string().max(100),
status: field.enum('UserStatus', ['active', 'inactive', 'banned'] as const).default('active'),
balance: field.decimal(10, 2).default('0.00'),
}) {}
// 2. Create storage model with PostgreSQL config
const PgUser = createPgModel(User, {
table: 'users', // Optional: defaults to snake_case + 's'
indexes: [
{ fields: ['email'], unique: true },
{ fields: ['status'] },
],
});
// 3. Create repository service
const UserRepository = createPgRepository(PgUser);
export { User, UserRepository };Storage Modes
The PostgreSQL adapter supports two storage modes:
Columnar Mode (Default)
Each field maps to a database column. Provides best query performance and full SQL capabilities.
import { createPgModel } from '@justscale/postgres';
import { User } from './user-model';
const PgUser = createPgModel(User, {
table: 'users',
storageMode: 'columnar', // Default, can be omitted
});
// Creates table: users (id, email, name, status, balance, created_at, updated_at, version)JSONB Mode
Fields are stored in a JSONB column. Useful for schema flexibility and rapid iteration. System fields (id, createdAt, updatedAt, version) remain as columns.
import { createPgModel } from '@justscale/postgres';
import { Product } from './product-model';
const PgProduct = createPgModel(Product, {
table: 'products',
storageMode: 'jsonb',
dataColumn: 'data', // Optional: defaults to 'data'
});
// Creates table: products (id, data, created_at, updated_at, version)
// All domain fields stored in the 'data' JSONB columnTable Naming Conventions
By default, table names are automatically generated from model names using snake_case and pluralization:
import { defineModel, field } from '@justscale/models';
import { createPgModel } from '@justscale/postgres';
class User extends defineModel({ name: field.string() }) {}
const PgUser = createPgModel(User);
// Table: 'users'
class BlogPost extends defineModel({ title: field.string() }) {}
const PgBlogPost = createPgModel(BlogPost);
// Table: 'blog_posts'
class Category extends defineModel({ name: field.string() }) {}
const PgCategory = createPgModel(Category, { table: 'categories' });
// Table: 'categories' (explicit override)Column Overrides
Override inferred PostgreSQL types or add constraints for specific fields:
import { defineModel, field } from '@justscale/models';
import { createPgModel } from '@justscale/postgres';
class User extends defineModel({
email: field.string().max(255),
tags: field.array(field.string()),
metadata: field.jsonb(),
}) {}
const PgUser = createPgModel(User, {
table: 'users',
overrides: {
email: { type: 'CITEXT', unique: true }, // Case-insensitive text
tags: { type: 'TEXT[]' }, // Native PostgreSQL array
},
});Connecting to PostgreSQL
Create a PostgreSQL client and register it with your cluster:
import { createPostgresClient } from '@justscale/postgres';
// Using connection string
const client = createPostgresClient({
connectionString: process.env.DATABASE_URL,
});
// Or with individual options
const client = createPostgresClient({
host: 'localhost',
port: 5432,
database: 'myapp',
username: 'user',
password: 'pass',
max: 10, // Connection pool size
idleTimeout: 20, // Seconds
connectTimeout: 10, // Seconds
});Type Safety
Repositories are fully typed based on your domain models:
import { User, UserRepository } from './user-model';
const UserService = createService({
inject: { users: UserRepository },
factory: ({ users }) => ({
async findActive() {
// Type-safe field references
return users.find({
where: User.fields.status.eq('active'),
orderBy: { createdAt: 'desc' },
});
// Returns: Persistent<User>[]
// Each entity has: id, email, name, status, balance, createdAt, updatedAt, version
},
}),
});Best Practices
- Separate domain from storage - Define models with
defineModel, then wrap withcreatePgModel - Use columnar mode for production - Better performance for queries, indexes, and constraints
- Use JSONB mode for rapid iteration - Schema changes without migrations during development
- Leverage type safety - Use field expressions like
User.fields.email.eq('...')for compile-time validation - Configure indexes - Add indexes for frequently queried fields to improve performance
What's Next
Now that you understand the basics, learn about repository methods, transactions, and advanced features: