Zynx is a powerful, Deno-native database migration system that transforms your DBML schema definitions into seamless PostgreSQL migrations. Just like how axolotls regenerate their limbs perfectly, Zynx regenerates your database schema with precision and grace.
- π Intelligent Schema Diffing: Automatically detects changes between DBML schemas and generates incremental migrations
- π Transaction Safety: All migrations run in atomic transactions with automatic rollback on failure
- 𧬠Multi-Format Configuration: Support for YAML, JSON (with
@atikayda/kjson), TypeScript, and JavaScript config files - π¦ Developer-Friendly CLI: Intuitive commands with helpful error messages and comprehensive help
- π― PostgreSQL Optimized: Built specifically for PostgreSQL with production-ready features
- π Deno Native: First-class TypeScript support with modern ESM imports
- π Migration Tracking: Comprehensive migration status and history tracking
- π Production Ready: Extensive error handling, validation, and recovery mechanisms
- π·οΈ Type Generation: Generate TypeScript, Go, Python, and Rust types from DBML schemas
- π Extension Features: Pre-configured support for common PostgreSQL extensions via the features system
# Install globally
deno install --allow-all -n zynx jsr:@atikayda/zynx/cli
# Or use directly
deno run --allow-all jsr:@atikayda/zynx/cli --help# Create a new Zynx project
zynx init
# This creates:
# - zynx.config.yaml # Configuration file
# - database.dbml # Schema definition
# - migrations/ # Migration directory- Define your schema in
database.dbml:
Table users {
id uuid [primary key, default: `gen_random_uuid()`]
email varchar(255) [unique, not null]
name varchar(100) [not null]
created_at timestamp [default: `now()`]
updated_at timestamp [default: `now()`]
}
Table posts {
id uuid [primary key, default: `gen_random_uuid()`]
title varchar(255) [not null]
content text
author_id uuid [ref: > users.id]
published boolean [default: false]
created_at timestamp [default: `now()`]
}
- Generate migrations:
zynx generate
# Creates: migrations/0001_initial_schema.sql- Apply migrations:
zynx run
# Applies all pending migrations to your database- Check status:
zynx status
# Shows applied and pending migrationsZynx automatically discovers configuration files in this order:
zynx.config.yaml(recommended)zynx.config.ymlzynx.config.jsonzynx.config.tszynx.config.js
# zynx.config.yaml
database:
type: postgresql
connectionString: "postgresql://postgres:password@localhost:5432/myapp"
ssl: false
migrations:
directory: "./migrations"
tableName: "zynx_migrations"
lockTimeout: 30000
queryTimeout: 60000
schema:
path: "./database.dbml"
format: "dbml"
encoding: "utf8"
generator:
addDropStatements: true
addIfNotExists: true
addComments: true
indent: " "
lineEnding: "\n"
# Optional: Enable features for PostgreSQL extensions
features:
- kjson # BigInt, Instant, and Duration support
- uuid-ossp # UUID generation functions// zynx.config.ts
import type { ZynxConfig } from "@atikayda/zynx/types";
export default {
database: {
type: "postgresql",
connectionString: Deno.env.get("DATABASE_URL")!,
ssl: Deno.env.get("NODE_ENV") === "production",
pool: {
min: 2,
max: 10,
timeout: 30000
}
},
migrations: {
directory: "./migrations",
tableName: "zynx_migrations"
},
schema: {
path: "./database.dbml"
},
features: ["kjson", "uuid-ossp"]
} satisfies ZynxConfig;Zynx uses @atikayda/kjson for enhanced JSON parsing with better error messages and BigInt support:
{
"database": {
"type": "postgresql",
"connectionString": "postgresql://localhost:5432/myapp",
"pool": {
"min": 2,
"max": 10,
"timeout": 30000
}
},
"migrations": {
"directory": "./migrations",
"tableName": "zynx_migrations",
"lockTimeout": 30000,
"queryTimeout": 60000
},
"schema": {
"path": "./database.dbml"
},
"generator": {
"addDropStatements": true,
"addIfNotExists": true,
"addComments": true
}
}Zynx includes a powerful features system that provides pre-configured support for common PostgreSQL extensions. Instead of manually configuring type mappings for each extension, you can simply enable features.
Provides seamless integration with the @atikayda/kjson package for handling BigInt, Instant, and Duration types in PostgreSQL.
features:
- kjsonThis feature automatically configures:
- Type mappings:
kinstantβInstant,kjsonβany,kdurationβDuration,decimal128βDecimal128 - Default functions:
kjson_now()for timestamp defaults - Proper imports in generated TypeScript code
Enables the uuid-ossp PostgreSQL extension for UUID generation.
features:
- uuid-osspThis feature provides:
- Automatic
CREATE EXTENSION IF NOT EXISTS "uuid-ossp"in migrations - UUID type mappings for all supported languages
- Default functions:
uuid_generate_v4()for UUID columns
Comprehensive support for PostGIS spatial data types and functions.
features:
- postgisIncludes:
- Geometry and geography type mappings
- Spatial indexes (GIST)
- Common spatial functions (ST_Distance, ST_Within, etc.)
- GeoJSON type mappings for TypeScript
Cryptographic functions for encryption, hashing, and random generation.
features:
- pgcryptoProvides:
- Functions:
gen_random_uuid(),digest(),crypt(), etc. - Can be used as an alternative to
uuid-osspfor UUID generation
Space-efficient probabilistic data structure for testing set membership.
features:
- bloomEnables:
- Bloom filter index type with configurable parameters
- Optimized for multi-column equality searches
# zynx.config.yaml
features:
- kjson
- uuid-ossp
- postgis
# Features automatically configure type mappings
# No need to manually specify typeMappings# Generate types with features
zynx generate-types --features kjson,uuid-ossp --language typescript
# Generate migrations with features
zynx generate --features kjson,uuid-ossp# zynx.config.yaml
database:
type: postgresql
connectionString: "postgresql://localhost:5432/myapp"
features:
- kjson # BigInt and time support
- uuid-ossp # UUID functions
- postgis # Spatial data
- pgcrypto # Encryption
schema:
path: "./database.dbml"Your DBML can then use these types naturally:
Table users {
id uuid [pk, default: `uuid_generate_v4()`]
location geometry(Point)
metadata kjson
created_at kinstant [default: `kjson_now()`]
password_hash text
}
Features can be combined with custom type mappings. Custom mappings take precedence:
features:
- kjson
schema:
typeMappings:
# Override kjson's default mapping
kinstant: "timestamp with time zone"
# Add custom type
money: "decimal(19,4)"# Generate migration from schema changes
zynx generate [options]
# Apply pending migrations
zynx run [options]
# Show migration status
zynx status [options]
# Initialize new project
zynx init
# Generate types from DBML schema
zynx generate-types --language typescript
# Show help
zynx --helpzynx generate # Generate from default schema
zynx generate --name "add-user-table" # Custom migration name
zynx generate --dry-run # Preview changes without creating files
zynx generate --schema "./custom.dbml" # Use custom schema file
zynx generate --force # Force generation even if no changes
zynx generate --features kjson,uuid-ossp # Generate with specific featureszynx run # Apply all pending migrations
zynx run --dry-run # Preview migrations without applying
zynx run --target 5 # Apply migrations up to specific number
zynx run --single # Apply only the next pending migration
zynx run --force # Force apply (use with caution)zynx status # Basic status overview
zynx status --verbose # Detailed status with migration lists
zynx status --json # JSON output for scriptingzynx generate-types --language typescript # Generate TypeScript types
zynx generate-types --language go # Generate Go structs
zynx generate-types --language python # Generate Python dataclasses
zynx generate-types --language rust # Generate Rust structs
zynx generate-types --all # Generate types for all languages
zynx generate-types --all --output ./types/ # Custom output directory
# With features
zynx generate-types --language typescript --features kjson,uuid-ossp
zynx generate-types --all --features kjson,postgis
# Language-specific options
zynx generate-types --language python --pydantic # Use Pydantic models
zynx generate-types --language rust --diesel # Include Diesel derives-c, --config <file> Use custom config file
-v, --verbose Enable verbose output
-h, --help Show help message
-V, --version Show version informationimport { ZynxManager, loadConfig } from "@atikayda/zynx";
// Load configuration
const config = await loadConfig("./zynx.config.yaml");
// Create manager instance
const zynx = new ZynxManager(config);
// Generate migrations
const generateResult = await zynx.generate();
console.log(`Generated: ${generateResult.migrationFile}`);
// Apply migrations
const runResult = await zynx.run();
console.log(`Applied ${runResult.appliedMigrations.length} migrations`);
// Check status
const status = await zynx.status();
console.log(`Pending: ${status.pendingMigrations.length}`);import {
ZynxManager,
loadConfig,
ZynxError,
DatabaseError,
SchemaError
} from "@atikayda/zynx";
try {
const config = await loadConfig();
const zynx = new ZynxManager(config);
// Generate with custom options
const result = await zynx.generate({
name: "add_user_profiles",
force: false
});
if (result.hasChanges) {
console.log(`Generated migration: ${result.migrationFile}`);
// Apply with dry run first
const dryRun = await zynx.run({ dryRun: true });
console.log("Dry run successful, applying migrations...");
const applied = await zynx.run();
console.log(`Successfully applied ${applied.appliedMigrations.length} migrations`);
} else {
console.log("No schema changes detected");
}
} catch (error) {
if (error instanceof SchemaError) {
console.error(`Schema error: ${error.message}`);
if (error.lineNumber) {
console.error(`At line ${error.lineNumber} in ${error.schemaPath}`);
}
} else if (error instanceof DatabaseError) {
console.error(`Database error: ${error.message}`);
if (error.query) {
console.error(`Query: ${error.query}`);
}
} else if (error instanceof ZynxError) {
console.error(`Zynx error: ${error.message}`);
} else {
console.error(`Unexpected error: ${error.message}`);
}
Deno.exit(1);
}import {
loadConfig,
createConfig,
discoverConfigFile,
isValidConfig
} from "@atikayda/zynx/config";
// Discover config file automatically
const configPath = await discoverConfigFile();
console.log(`Found config: ${configPath}`);
// Load from specific path
const config = await loadConfig("./custom-config.yaml");
// Create config programmatically
const config = createConfig({
database: {
type: "postgresql",
connectionString: "postgresql://localhost:5432/myapp"
},
migrations: {
directory: "./db/migrations"
},
schema: {
path: "./schema/database.dbml"
}
});
// Validate configuration
if (isValidConfig(config)) {
console.log("Configuration is valid");
}import {
DBMLParser,
TypeScriptGenerator,
GoGenerator,
PythonGenerator,
RustGenerator
} from "@atikayda/zynx";
// Parse DBML schema with features
const parser = new DBMLParser({
typeMappings: {
kinstant: "kinstant",
kjson: "kjson",
uuid: "uuid"
}
});
const schema = await parser.parse(dbmlContent);
// Generate TypeScript types with feature support
const tsGenerator = new TypeScriptGenerator({
addComments: true,
enumStyle: "union",
dateHandling: "Date",
customTypes: {
kinstant: "Instant",
kjson: "any",
uuid: "string"
},
imports: {
Instant: "@atikayda/kjson"
}
});
const tsTypes = tsGenerator.generateFile(schema);
// Generate Go structs
const goGenerator = new GoGenerator({
namespace: "models"
});
const goTypes = goGenerator.generateFile(schema);
// Generate Python dataclasses
const pyGenerator = new PythonGenerator({
pythonStyle: "pydantic"
});
const pyTypes = pyGenerator.generateFile(schema);
// Generate Rust structs
const rustGenerator = new RustGenerator({
useSerde: true,
deriveTraits: ["Debug", "Clone", "Serialize", "Deserialize"]
});
const rustTypes = rustGenerator.generateFile(schema);import {
validateFeatures,
listFeatures,
getLanguageTypeMappings,
getSQLTypeMappings
} from "@atikayda/zynx/features";
// List all available features
const features = listFeatures();
console.log("Available features:");
features.forEach(f => {
console.log(`- ${f.name}: ${f.description}`);
});
// Validate feature names
try {
validateFeatures(["kjson", "uuid-ossp"]);
console.log("Features are valid!");
} catch (error) {
console.error("Invalid feature:", error.message);
}
// Get type mappings for a specific language
const { customTypes, imports } = getLanguageTypeMappings(
["kjson", "uuid-ossp"],
"typescript"
);
console.log("TypeScript types:", customTypes);
console.log("Required imports:", imports);
// Get SQL type mappings for DBML parser
const sqlMappings = getSQLTypeMappings(["kjson", "postgis"]);
console.log("SQL mappings:", sqlMappings);Generated migration files follow a consistent structure:
-- Migration: 0001_add_user_table
-- Generated: 2024-01-15T10:30:00Z
-- Zynx Version: 1.0.0
BEGIN;
-- Create users table
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT now(),
updated_at TIMESTAMP DEFAULT now()
);
-- Create indexes
CREATE INDEX idx_users_email ON users(email);
-- Insert migration record
INSERT INTO zynx_migrations (number, name, checksum, executed_at)
VALUES (1, 'add_user_table', 'sha256:abc123...', now());
COMMIT;When features are enabled, Zynx intelligently manages extensions:
The first migration includes all necessary extensions:
-- Migration: 0001_initial_schema
-- Generated: 2024-01-15T10:30:00Z
-- Zynx Version: 1.0.0
-- Features: kjson, uuid-ossp
BEGIN;
-- Create extensions (from features)
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Create users table with feature types
CREATE TABLE users (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
metadata kjson,
created_at kinstant DEFAULT kjson_now(),
updated_at kinstant DEFAULT kjson_now()
);
COMMIT;Regular migrations don't include extension statements:
-- Migration: 0002_add_posts_table
-- Generated: 2024-01-16T10:30:00Z
BEGIN;
CREATE TABLE posts (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
title text NOT NULL,
created_at kinstant DEFAULT kjson_now()
);
COMMIT;When you add a new feature requiring extensions:
-- Migration: 0003_add_location_support
-- Generated: 2024-01-17T10:30:00Z
-- New Features: postgis
BEGIN;
-- Create newly required extensions
CREATE EXTENSION IF NOT EXISTS "postgis";
CREATE EXTENSION IF NOT EXISTS "postgis_topology";
-- Add location column using new feature
ALTER TABLE users ADD COLUMN location geometry(Point);
COMMIT;name: Database Migrations
on: [push, pull_request]
jobs:
migrate:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: denoland/setup-deno@v2
with:
deno-version: v1.x
- name: Run migrations (dry run)
run: deno run --allow-all jsr:@atikayda/zynx/cli run --dry-run
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
- name: Apply migrations
run: deno run --allow-all jsr:@atikayda/zynx/cli run
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testFROM denoland/deno:alpine
WORKDIR /app
COPY . .
# Cache dependencies
RUN deno cache jsr:@atikayda/zynx/cli
# Run migrations
ENTRYPOINT ["deno", "run", "--allow-all", "jsr:@atikayda/zynx/cli"]
CMD ["run"]version: '3.8'
services:
postgres:
image: postgres:15
environment:
POSTGRES_DB: myapp
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
migrate:
image: denoland/deno:alpine
depends_on:
- postgres
volumes:
- .:/app
working_dir: /app
environment:
DATABASE_URL: postgresql://postgres:password@postgres:5432/myapp
command: >
sh -c "
deno run --allow-all jsr:@atikayda/zynx/cli run
"Migration fails with connection error:
# Check your database connection
zynx status --verbose
# Test with different connection string
zynx status --config ./test-config.yamlSchema parsing errors:
# Validate your DBML syntax
zynx generate --dry-run --verbose
# Check specific schema file
zynx generate --schema ./problematic-schema.dbml --dry-runMigration conflicts:
# Show detailed migration status
zynx status --verbose
# Force regenerate (careful!)
zynx generate --force# Enable verbose logging
ZYNX_LOG_LEVEL=debug zynx run --verbose
# Show configuration resolution
zynx status --verbose# Run all tests
deno test --allow-all
# Run specific test suite
deno test --allow-all tests/cli/
deno test --allow-all tests/core/
# Run with coverage
deno test --allow-all --coverage=cov_profile
deno coverage cov_profileWe welcome contributions! Please see our Contributing Guide for details.
# Clone the repository
git clone https://github.com/atikayda/zynx.git
cd zynx
# Run tests
deno test --allow-all
# Run CLI locally
deno run --allow-all ./cli.ts --help
# Format code
deno fmt
# Lint code
deno lintMIT License - see LICENSE for details.
The axolotl is famous for its incredible regenerative abilities - it can regrow entire limbs, organs, and even parts of its brain with perfect precision. This makes it the perfect mascot for a migration system that regenerates database schemas seamlessly and reliably.
Made with π for the Deno ecosystem