Skip to content

CLI Reference

The Melange CLI provides commands for validating schemas, generating client code, and applying migrations to your database. Built on Cobra/Viper, it supports configuration files, environment variables, and command-line flags with consistent precedence.

Installation

go install github.com/pthm/melange/cmd/melange@latest

Global Flags

These flags are available on all commands:

FlagDescription
--configPath to config file (default: auto-discover melange.yaml). See Configuration.
-v, --verboseIncrease verbosity (can be repeated: -vv, -vvv)
-q, --quietSuppress non-error output
--no-update-checkDisable automatic update checking

Update Notifications

By default, Melange automatically checks for new versions in the background and displays a notification if an update is available. The check is:

  • Non-blocking: Runs in a background goroutine with a 5-second timeout
  • Cached: Results are cached for 24 hours in ~/.cache/melange/update-check.json
  • Fast: Uses a 1-second wait time for displaying results
  • Respectful: Automatically disabled in CI environments (when CI env var is set)

Example notification:

$ melange migrate
Migration completed successfully

* A new version of melange is available: v1.2.3 (current: v1.2.0)
  brew upgrade melange  or  go install github.com/pthm/melange/cmd/melange@latest

To disable update checks:

melange --no-update-check migrate

To clear the cache:

rm -rf ~/.cache/melange/update-check.json

The cache respects the XDG_CACHE_HOME environment variable if set.

Command Groups

Commands are organized into logical groups:

Schema Commands: validate, migrate, status, doctor Client Commands: generate client, generate migration Utility Commands: init, config, version, license


Schema Commands

validate

Check .fga schema syntax without database access.

melange validate --schema schemas/schema.fga

Output:

Schema is valid. Found 3 types:
  - user (0 relations)
  - organization (3 relations)
  - repository (5 relations)

This command parses the schema using the OpenFGA parser and reports any syntax errors. It does not require database access.

Flags:

FlagDefaultDescription
--schemaschemas/schema.fgaPath to schema.fga file

The schema path can also be set via configuration file or environment variable. See Configuration.

migrate

Apply the schema to your PostgreSQL database.

melange migrate \
  --db postgres://localhost/mydb \
  --schema schemas/schema.fga

Flags:

FlagDefaultDescription
--db(from config)PostgreSQL connection string
--db-schema""PostgreSQL schema for melange objects
--schemaschemas/schema.fgaPath to schema.fga file
--dry-runfalseOutput SQL to stdout without applying changes
--forcefalseForce migration even if schema is unchanged

This command:

  1. Checks if the schema has changed since the last migration
  2. Installs generated SQL functions (check_permission, list_accessible_objects, etc.)
  3. Cleans up orphaned functions from removed relations
  4. Records the migration in melange_migrations table

Skip-if-unchanged behavior:

Melange tracks schema changes using a SHA256 checksum. If you run migrate and the schema hasn’t changed since the last migration, it will be skipped automatically:

Schema unchanged, migration skipped.
Use --force to re-apply.

Use --force to re-apply the migration anyway (useful after updating Melange itself).

Dry-run mode:

Preview the migration SQL without applying it:

melange migrate --db postgres://localhost/mydb --dry-run

This outputs the complete SQL that would be executed, including:

  • DDL for the migrations tracking table
  • All generated check functions
  • All generated list functions
  • Dispatcher functions
  • The migration record insert

Dry-run output goes to stdout, so you can redirect it:

melange migrate --db postgres://localhost/mydb --dry-run > migration.sql

Orphan cleanup:

When you remove a relation from your schema, Melange automatically drops the orphaned SQL functions during migration. For example, if you remove the editor relation from document, the next migration will drop check_document_editor, list_document_editor_objects, etc.

melange_tuples warning:

After migration, if the melange_tuples view doesn’t exist, you’ll see a warning:

WARNING: melange_tuples view/table does not exist.
         Permission checks will fail until you create it.

See Tuples View for setup instructions.

status

Check the current migration status.

melange status \
  --db postgres://localhost/mydb \
  --schema schemas/schema.fga

Flags:

FlagDefaultDescription
--db(from config)PostgreSQL connection string
--db-schema""Database schema
--schemaschemas/schema.fgaPath to schema.fga file

Output:

Schema file:  present
Tuples view:  present

This helps you verify that:

  • Your schema file exists
  • The tuples view exists in the database

doctor

Run comprehensive health checks on your authorization infrastructure.

melange doctor \
  --db postgres://localhost/mydb \
  --schema schemas/schema.fga

Flags:

FlagDefaultDescription
--db(from config)PostgreSQL connection string
--db-schema""Database schema
--schemaschemas/schema.fgaPath to schema.fga file
--verbosefalseShow detailed output with additional context
--skip-performancefalseSkip performance checks (view analysis)

Output:

melange doctor - Health Check

Schema File
  ✓ Schema file exists at schemas/schema.fga
  ✓ Schema is valid (3 types, 8 relations)
  ✓ No cyclic dependencies detected

Migration State
  ✓ melange_migrations table exists
  ✓ Schema migrated (24 functions tracked)
  ✓ Schema is in sync with database

Generated Functions
  ✓ All dispatcher functions present
  ✓ All 24 expected functions present

Tuples Source
  ✓ melange_tuples exists (view)
  ✓ All required columns present

Data Health
  ✓ melange_tuples contains 1523 tuples
  ✓ All sampled tuples reference valid types and relations

Performance
  ✓ View definition parsed (3 branches)
  ✓ View uses UNION ALL (no unnecessary deduplication)
  ✓ Source tables: users, organizations, repositories
  ✓ All ::text cast columns have expression indexes

Summary: 15 passed, 0 warnings, 0 errors

The doctor command performs the following checks:

Schema File:

  • Verifies the schema file exists
  • Parses and validates schema syntax
  • Detects cyclic dependencies in implied-by relationships

Migration State:

  • Checks if the melange_migrations tracking table exists
  • Verifies a migration has been applied
  • Compares schema checksum to detect if schema has changed since last migration
  • Checks if codegen version has changed (indicating Melange was updated)

Generated Functions:

  • Verifies all dispatcher functions exist (check_permission, list_accessible_objects, etc.)
  • Compares expected functions from schema against actual functions in database
  • Identifies orphan functions from previous schema versions

Tuples Source:

  • Verifies melange_tuples view/table exists
  • Checks required columns: object_type, object_id, relation, subject_type, subject_id
  • Warns if using a materialized view (requires manual refresh)

Data Health:

  • Reports tuple count
  • Validates that tuples reference valid types and relations defined in the schema

Performance (view-based melange_tuples only):

Performance checks run automatically when melange_tuples is a view (not a table or materialized view). They can be disabled with --skip-performance.

  • Parses the view definition via pg_get_viewdef() and reports the number of UNION branches and source tables
  • Detects bare UNION (warns to use UNION ALL instead, since UNION adds deduplication overhead)
  • Checks for missing expression indexes on columns cast with ::text in the view definition. Missing indexes cause sequential scans on the source tables:
    • Warning if the source table has fewer than 10,000 rows (recommended for future scaling)
    • Failure if the source table has 10,000+ rows (critical at current scale)
    • Provides exact CREATE INDEX statements as fix hints

Verbose mode:

Use --verbose to see additional details for each check:

melange doctor --db postgres://localhost/mydb --verbose

This shows:

  • Exact file paths and checksums
  • Lists of missing or orphan functions
  • Specific unknown types or relations found in data

Common issues and fixes:

IssueFix
Schema file not foundCreate schemas/schema.fga
Schema has syntax errorsRun fga model validate for detailed errors
Schema out of syncRun melange migrate
Missing functionsRun melange migrate
Orphan functionsRun melange migrate (cleanup is automatic)
melange_tuples missingCreate a view over your domain tables
Missing columnsUpdate melange_tuples to include all required columns
Unknown types in tuplesUpdate tuples view or schema to match
UNION instead of UNION ALLReplace UNION with UNION ALL in view definition
Missing expression indexRun the CREATE INDEX command shown in the fix hint

Client Commands

generate client

Generate type-safe client code from your schema.

melange generate client \
  --runtime go \
  --schema schemas/schema.fga \
  --output internal/authz \
  --package authz

Flags:

FlagDefaultDescription
--runtime(required)Target runtime: go, typescript
--schemaschemas/schema.fgaPath to schema.fga file
--outputstdoutOutput directory for generated code
--packageauthzPackage name for generated code
--id-typestringID type for constructors (string, int64, uuid.UUID)
--filter""Only generate relations with this prefix (e.g., can_)

Example with all options:

melange generate client \
  --runtime go \
  --schema schemas/schema.fga \
  --output internal/authz \
  --package authz \
  --id-type int64 \
  --filter can_

Output to stdout:

melange generate client --runtime go --schema schemas/schema.fga

Generated code example:

// schema_gen.go
package authz

import "github.com/pthm/melange/melange"

// Object types
const (
    TypeUser         melange.ObjectType = "user"
    TypeOrganization melange.ObjectType = "organization"
    TypeRepository   melange.ObjectType = "repository"
)

// Relation constants (filtered by prefix "can_")
const (
    RelCanRead   melange.Relation = "can_read"
    RelCanWrite  melange.Relation = "can_write"
    RelCanDelete melange.Relation = "can_delete"
)

// Type-safe constructors
func User(id int64) melange.Object {
    return melange.Object{Type: TypeUser, ID: fmt.Sprint(id)}
}

func Repository(id int64) melange.Object {
    return melange.Object{Type: TypeRepository, ID: fmt.Sprint(id)}
}

// Wildcard constructors
func AnyUser() melange.Object {
    return melange.Object{Type: TypeUser, ID: "*"}
}

Supported runtimes:

RuntimeStatusDescription
goImplementedType-safe Go code with constants and constructors
typescriptPlannedTypeScript types and factory functions

generate migration

Generate versioned SQL migration files for use with external migration frameworks (golang-migrate, Atlas, Flyway, etc.). Instead of applying SQL directly like melange migrate, this command produces .sql files you commit, review, and apply through your existing workflow.

For a conceptual overview of when to use this versus melange migrate, see Running Migrations.

melange generate migration \
  --schema schemas/schema.fga \
  --output db/migrations

Flags:

FlagDefaultDescription
--schemaschemas/schema.fgaPath to current .fga schema file (required)
--db-schema""PostgreSQL schema for melange objects
--output(stdout)Output directory for migration files
--namemelangeMigration name suffix used in filenames
--formatsplitOutput format: split (.up.sql/.down.sql) or single
--upfalseOutput only the UP migration (stdout mode only)
--downfalseOutput only the DOWN migration (stdout mode only)
--db-Database URL. Compare against most recent migration record
--git-ref-Git ref. Compare against schema at that commit/branch/tag
--previous-schema-File path. Compare against a previous .fga file
The three comparison flags (--db, --git-ref, --previous-schema) are mutually exclusive. When none is specified, a full migration is generated containing all functions.

Output modes:

When --output is specified, timestamped files are written to that directory:

db/migrations/20260322143000_melange.up.sql
db/migrations/20260322143000_melange.down.sql

When --output is omitted (stdout mode), you must specify --up or --down to select which migration to print. This is useful for piping into other tools:

melange generate migration --schema schemas/schema.fga --git-ref main --up | psql "$DATABASE_URL"

Output formats:

FormatFilesUse case
splitTIMESTAMP_NAME.up.sql, TIMESTAMP_NAME.down.sqlgolang-migrate, Atlas, most frameworks
singleTIMESTAMP_NAME.sql with UP and DOWN sectionsFlyway, frameworks expecting a single file

Comparison modes:

By default, the UP migration includes every generated function (full mode). To emit only what changed, use one of:

  • --db - Reads the previous function inventory from the melange_migrations table. Most precise, but requires database access.
  • --git-ref - Compiles the schema from a git ref and compares. No database needed, suitable for CI.
  • --previous-schema - Compiles a previous schema from a local file and compares.

When a comparison mode is active:

  • Only functions with changed SQL bodies (or newly added) are included in UP
  • Orphaned functions (removed from schema) get DROP FUNCTION IF EXISTS statements
  • Dispatcher functions are always included regardless of changes

Examples:

# First migration (full - all functions)
melange generate migration \
  --schema schemas/schema.fga \
  --output db/migrations

# Incremental migration comparing against database
melange generate migration \
  --schema schemas/schema.fga \
  --output db/migrations \
  --db postgres://localhost/mydb

# Incremental migration comparing against git
melange generate migration \
  --schema schemas/schema.fga \
  --output db/migrations \
  --git-ref main

# Preview changes to stdout
melange generate migration \
  --schema schemas/schema.fga \
  --git-ref HEAD~1 \
  --up

# Single-file format for Flyway
melange generate migration \
  --schema schemas/schema.fga \
  --output db/migrations \
  --format single \
  --git-ref main

Utility Commands

init

Initialize a new Melange project with an interactive wizard. Detects your project type (Go or TypeScript) and scaffolds a config file, starter schema, and runtime dependency.

melange init

Flags:

FlagDefaultDescription
-y, --yesfalseAccept all defaults without prompting
--no-installfalseSkip installing runtime dependencies
--schemamelange/schema.fgaSchema file path
--dbpostgres://localhost:5432/mydbDatabase URL
--templateorg-rbacStarter model: org-rbac, doc-sharing, minimal, none
--runtime(auto-detected)Client runtime: go, typescript
--outputinternal/authz (Go) / src/authz (TS)Client output directory
--packageauthzClient package name (Go only)
--id-typestringClient ID type: string, int64, uuid.UUID

Project detection:

Melange inspects the current directory for project files:

FileDetected RuntimeDefault Output
go.modGointernal/authz
package.jsonTypeScriptsrc/authz

When a project is detected, client code generation is enabled by default and the runtime dependency is installed automatically (go get github.com/pthm/melange/melange for Go, npm install @pthm/melange for TypeScript). If both go.mod and package.json exist, Go takes precedence.

For TypeScript projects, the package manager is auto-detected from lock files: bun.lockb/bun.lock → bun, pnpm-lock.yaml → pnpm, yarn.lock → yarn, otherwise npm.

Starter models:

TemplateDescription
org-rbacOrganization with role hierarchy and repository permissions (default)
doc-sharingGoogle Docs-style owner/editor/viewer model
minimalBare-bones model with just type user
noneSkip schema file creation

Directory convention:

By default, init creates a melange/ directory containing both the config and schema:

myproject/
├── melange/
│   ├── config.yaml
│   └── schema.fga
└── ...

If the schema path is outside the melange/ directory (e.g., --schema schemas/auth.fga), the config is placed at the project root as melange.yaml instead.

Safety:

  • Refuses to overwrite an existing config file in --yes mode (use interactive mode to confirm)
  • Skips schema creation if the file already exists in --yes mode

Examples:

# Interactive wizard
melange init

# Accept all defaults (in a Go project)
melange init -y

# Custom schema and database, skip dependency install
melange init -y --schema melange/auth.fga --db postgres://prod:5432/app --no-install

# Minimal schema with no client generation
melange init -y --template minimal

config show

Display the effective configuration after merging defaults, config file, and environment variables.

melange config show

Flags:

FlagDefaultDescription
--sourcefalseShow the config file path being used

Example with source:

melange config show --source

Output:

Config file: /path/to/project/melange.yaml

schema: schemas/schema.fga
database:
  url: postgres://localhost/mydb
  host: ""
  port: 5432
  ...

This is useful for debugging configuration issues and understanding which values are in effect.

version

Print version information.

melange version

Output:

melange v1.0.0 (commit: abc1234, built: 2024-01-15)

license

Print license and third-party notices.

melange license

This displays the Melange license and attribution for all embedded third-party dependencies.


Exit Codes

CodeMeaning
0Success
1General error (validation, runtime, IO)
2Configuration error (invalid config file, missing required settings)
3Schema parse error
4Database connection error

Common Workflows

Development Setup

With melange init (recommended):

melange init

This creates a config file and starter schema. Then run commands without flags:

# Validate schema
melange validate

# Apply to database
melange migrate

# Generate Go code
melange generate client

Without configuration file:

# 1. Validate schema syntax
melange validate --schema schemas/schema.fga

# 2. Apply to local database
melange migrate \
  --db postgres://localhost/myapp_dev \
  --schema schemas/schema.fga

# 3. Generate Go code
melange generate client \
  --runtime go \
  --schema schemas/schema.fga \
  --output internal/authz \
  --package authz

CI/CD Pipeline

Use environment variables for credentials:

# Set database URL from CI secrets
export MELANGE_DATABASE_URL="$DATABASE_URL"

# Validate schema (fails fast if syntax error)
melange validate

# Preview migration (optional, for review)
melange migrate --dry-run

# Apply migrations
melange migrate

# Run health checks
melange doctor

For pipelines where you want to ensure migrations are always applied (e.g., after a Melange version update):

melange migrate --force

Schema Updates

When you modify your .fga schema:

# 1. Validate changes
melange validate

# 2. Regenerate client code
melange generate client

# 3. Apply to database
melange migrate

Or as a single workflow with explicit flags:

melange validate --schema schemas/schema.fga && \
  melange generate client --runtime go --output internal/authz && \
  melange migrate --db "$DATABASE_URL"

External Migration Frameworks

If you use golang-migrate, Atlas, Flyway, or a similar tool, replace melange migrate with melange generate migration:

# First time - generate a full migration
melange generate migration \
  --schema schemas/schema.fga \
  --output db/migrations

# After schema changes - generate an incremental migration
melange generate migration \
  --schema schemas/schema.fga \
  --output db/migrations \
  --git-ref main

# Commit the schema change and migration together
git add schemas/schema.fga db/migrations/
git commit -m "Add editor relation to document type"

# Apply with your framework
migrate -path db/migrations -database "$DATABASE_URL" up

See Running Migrations for guidance on choosing between built-in and external migration strategies.

Troubleshooting

When permission checks aren’t working as expected, use doctor to diagnose issues:

# Run comprehensive health checks
melange doctor

# With verbose output for more details
melange doctor --verbose

# Check effective configuration
melange config show --source

Common scenarios where doctor helps:

  1. Permission checks returning unexpected results - Doctor validates that your schema, generated functions, and tuples are all in sync.

  2. After updating Melange - Doctor detects if the codegen version has changed and functions need regenerating.

  3. New environment setup - Doctor validates the complete authorization stack is properly configured.

  4. Data migration issues - Doctor samples tuples and validates they reference valid types and relations.

  5. Slow permission checks - Doctor detects missing expression indexes and inefficient view patterns that cause performance degradation at scale.


Programmatic Alternative

For programmatic schema management without the CLI, use the Go API:

import (
    "github.com/pthm/melange/pkg/parser"
    "github.com/pthm/melange/pkg/migrator"
)

// Parse schema
types, err := parser.ParseSchema("schemas/schema.fga")
if err != nil {
    log.Fatal(err)
}

// Create migrator and apply
m := migrator.NewMigrator(db, "schemas/schema.fga")
err = m.MigrateWithTypes(ctx, types)

With options (dry-run, force, skip-if-unchanged):

import (
    "os"
    "github.com/pthm/melange/pkg/migrator"
)

// Dry-run: output SQL to stdout
opts := migrator.MigrateOptions{
    DryRun: os.Stdout,
}
skipped, err := migrator.MigrateWithOptions(ctx, db, "schemas/schema.fga", opts)

// Force migration even if unchanged
opts := migrator.MigrateOptions{
    Force: true,
}
skipped, err := migrator.MigrateWithOptions(ctx, db, "schemas/schema.fga", opts)

// Normal migration with skip detection
opts := migrator.MigrateOptions{}
skipped, err := migrator.MigrateWithOptions(ctx, db, "schemas/schema.fga", opts)
if skipped {
    log.Println("Schema unchanged, migration skipped")
}

Running health checks programmatically:

import (
    "os"
    "github.com/pthm/melange/lib/doctor"
)

d := doctor.New(db, "schemas/schema.fga", doctor.Options{})
report, err := d.Run(ctx)
if err != nil {
    log.Fatal(err)
}

// Print report (verbose=true for detailed output)
report.Print(os.Stdout, true)

// Check for critical failures
if report.HasErrors() {
    os.Exit(1)
}

// Access individual check results
for _, check := range report.Checks {
    if check.Status == doctor.StatusFail {
        log.Printf("FAIL: %s - %s", check.Name, check.Message)
        if check.FixHint != "" {
            log.Printf("  Fix: %s", check.FixHint)
        }
    }
}

See Checking Permissions for the full Go API reference.