Skip to content

demetere/omg

Repository files navigation

OpenFGA Migration Tool (OMG)

A model-first migration tool for OpenFGA authorization models and relationship tuples. Inspired by database migration tools like Prisma and Entity Framework, OMG automatically generates migrations from your authorization model changes.

Think "Prisma for OpenFGA" - edit your model, generate migrations, apply changes.

✨ Features

Model-First Workflow

  • Automatic migration generation from model.fga changes
  • Intelligent rename detection with confidence levels (High/Medium/Low)
  • Multi-factor analysis - considers both name and relation structure similarity
  • Context-aware templates - generated code reflects confidence in changes
  • Safe by default - prevents accidental data loss with smart defaults

Migration Management

  • Version-controlled migrations - Track changes to authorization models and tuples
  • Up/Down migrations - Apply and rollback changes safely
  • Migration tracking via tuples - Uses OpenFGA itself to track migration history
  • Direct API queries - Queries OpenFGA for current model state (no cache files)

Developer Experience

  • Rich helper functions - Common operations like rename, copy, transform
  • Batch operations - Efficiently handle large numbers of tuples
  • Comprehensive testing - Unit and integration tests with testcontainers
  • CLI interface - Simple commands: diff, generate, up, down

πŸš€ Quick Start (Model-First)

1. Installation

# Clone the repository
git clone https://github.com/demetere/omg.git
cd omg

# Build the CLI
go build -o omg ./cmd/omg

# Optional: Install globally
go install ./cmd/omg

2. Configure Environment

Create a .env file or set environment variables:

OPENFGA_API_URL=http://localhost:8080
OPENFGA_STORE_ID=your-store-id
OPENFGA_AUTH_METHOD=none

For production with authentication:

OPENFGA_API_URL=https://api.fga.example
OPENFGA_STORE_ID=01HXYZ...
OPENFGA_AUTH_METHOD=token
OPENFGA_API_TOKEN=your-api-token

3. Create Your Authorization Model

Create or edit model.fga:

model
  schema 1.1

type user

type document
  relations
    define owner: [user]
    define editor: [user]
    define viewer: [user] or editor

4. See What Changed

./omg diff

Output:

Comparing model.fga with current state...

Detected 2 change(s):

+ New type 'document' with 3 relations
+ New type 'user' with 0 relations

Run 'omg generate <name>' to create a migration for these changes

5. Generate Migration

./omg generate initial_model

Output:

Generating migration...

βœ“ Migration created: migrations/20241128150000_initial_model.go

Next steps:
  1. Review the generated migration file
  2. Edit if needed
  3. Run 'omg up' to apply the migration

6. Review & Apply

# Review the generated code
cat migrations/20241128150000_initial_model.go

# Apply the migration
./omg up

🎯 Model-First Workflow Examples

Example 1: High Confidence Rename

Scenario: Rename document β†’ documents (very similar names)

# Edit model.fga: change 'type document' to 'type documents'
vim model.fga

# Check changes
./omg diff

Output:

β†’ Rename detected: 'document' -> 'documents' (high confidence: 88% name, 100% relations)
    Old: document
    New: documents
# Generate migration
./omg generate pluralize_documents

Generated code (clean rename):

// Rename type: document -> documents (high confidence rename detected)
// This will migrate all existing tuples to the new type name
if err := omg.RenameType(ctx, client, "document", "documents"); err != nil {
    return fmt.Errorf("failed to rename type: %w", err)
}

βœ… High confidence = clean code, usually safe to apply


Example 2: Medium Confidence Rename

Scenario: Rename document β†’ file with same relations

# Edit model.fga: change 'type document' to 'type file'
vim model.fga

./omg diff

Output:

β†’ Possible rename: 'document' -> 'file' (medium confidence - review required)
    Old: document
    New: file
./omg generate rename_to_file

Generated code (with review warning):

// ⚠️  REVIEW REQUIRED: Possible rename detected
// Detected: document -> file
//
// This appears to be a rename based on similarity analysis.
// If this IS a rename (preserving tuples), keep the code below.
// If these are separate types, replace with AddType + DeleteType operations.
//
if err := omg.RenameType(ctx, client, "document", "file"); err != nil {
    return fmt.Errorf("failed to rename type: %w", err)
}

⚠️ Medium confidence = review required, likely correct


Example 3: Low Confidence or No Detection

Scenario: Very different names or no relation similarity

./omg diff

Output:

- Type 'document' removed
+ New type 'asset' with 3 relations
./omg generate separate_types

Generated code (safe add+remove):

// Add new type (already in model.fga)
if err := omg.AddTypeToModel(ctx, client, "asset", relations); err != nil {
    return fmt.Errorf("failed to add type: %w", err)
}

// Remove old type and tuples
tuples, err := omg.ReadAllTuples(ctx, client, "document", "")
if err != nil {
    return fmt.Errorf("failed to read tuples: %w", err)
}
// ... deletion code

βœ… Low/no confidence = safe operations, prevents data loss

πŸ“‹ CLI Commands

Model-First Commands

diff

Compare model.fga with current state:

./omg diff

generate <name>

Generate migration from detected changes:

./omg generate add_folders

init <store-name>

Initialize tracking for a store:

./omg init my-store

Migration Commands

up

Apply all pending migrations:

./omg up

down

Rollback the last migration:

./omg down

status

Show migration status:

./omg status

Output:

Migration Status:
================

20241128150000  initial_model                     APPLIED
20241128151000  add_folders                       PENDING

Total: 2 migrations (1 applied, 1 pending)

Utility Commands

show-model

Display current authorization model:

./omg show-model

list-tuples [type]

List tuples, optionally filtered by type:

./omg list-tuples
./omg list-tuples document

list-stores

List available OpenFGA stores:

./omg list-stores

πŸ”§ Advanced: Manual Migrations

For complex data operations that can't be auto-generated, create manual migrations:

Create Manual Migration

./omg create custom_data_migration

Edit Migration

package migrations

import (
    "context"
    "fmt"
    "strings"

    "github.com/demetere/omg/pkg"
)

func init() {
    omg.Register(omg.Migration{
        Version: "20241128160000",
        Name:    "custom_data_migration",
        Up:      up_20241128160000,
        Down:    down_20241128160000,
    })
}

func up_20241128160000(ctx context.Context, client *omg.Client) error {
    // Custom transformation: migrate user ID format
    transform := func(tuple openfgaSdk.Tuple) (openfgaSdk.Tuple, error) {
        // Change user:123 -> user:uuid-123
        if strings.HasPrefix(tuple.User, "user:") {
            id := strings.TrimPrefix(tuple.User, "user:")
            tuple.User = "user:uuid-" + id
        }
        return tuple, nil
    }

    return omg.TransformAllTuples(ctx, client, "document", "viewer", transform)
}

func down_20241128160000(ctx context.Context, client *omg.Client) error {
    // Reverse transformation
    transform := func(tuple openfgaSdk.Tuple) (openfgaSdk.Tuple, error) {
        if strings.HasPrefix(tuple.User, "user:uuid-") {
            id := strings.TrimPrefix(tuple.User, "user:uuid-")
            tuple.User = "user:" + id
        }
        return tuple, nil
    }

    return omg.TransformAllTuples(ctx, client, "document", "viewer", transform)
}

🧠 How Confidence Levels Work

OMG uses multi-factor analysis to determine rename confidence:

Factors Analyzed

  1. Name Similarity (Levenshtein distance)

    • team β†’ teams: 80% similar
    • document β†’ file: 30% similar
    • team β†’ organization: 8% similar
  2. Relation Structure (Jaccard coefficient)

    • Same relations: 100% similar
    • Partial overlap: 50-99% similar
    • No overlap: 0% similar

Confidence Thresholds

Confidence Criteria Action
High Name β‰₯70% OR (Name β‰₯40% AND Relations β‰₯70%) Clean rename code
Medium Name β‰₯30% OR Relations β‰₯70% Rename with warning
Low Name β‰₯20% OR Relations β‰₯50% Both options provided
None Below thresholds Separate add+remove

Examples

team β†’ teams (80% name, 100% relations)           = High
team β†’ organization (8% name, 100% relations)     = Medium
document β†’ file (30% name, 0% relations)          = Medium
user β†’ person (40% name, 0% relations)            = Low
team β†’ asset (0% name, 0% relations)              = None

πŸ› οΈ Helper Functions

The generated migrations use these helper functions (you can use them in manual migrations too):

Type Operations

// Rename a type and migrate all tuples
omg.RenameType(ctx, client, "team", "organization")

// Add a type to the model
omg.AddTypeToModel(ctx, client, "folder", relations)

// Remove a type from the model
omg.RemoveTypeFromModel(ctx, client, "team")

Relation Operations

// Rename a relation
omg.RenameRelation(ctx, client, "document", "can_view", "viewer")

// Copy tuples to a new relation
omg.CopyRelation(ctx, client, "document", "editor", "can_edit")

// Delete all tuples of a relation
omg.DeleteRelation(ctx, client, "document", "deprecated_relation")

// Add a relation to a type
omg.AddRelationToType(ctx, client, "document", "commenter", "[user]")

// Remove a relation from a type
omg.RemoveRelationFromType(ctx, client, "document", "commenter")

// Update a relation definition
omg.UpdateRelationDefinition(ctx, client, "document", "viewer", "[user] or editor")

Tuple Operations

// Read all tuples matching criteria
tuples, err := omg.ReadAllTuples(ctx, client, "document", "viewer")

// Count tuples
count, err := omg.CountTuples(ctx, client, "document", "owner")

// Transform tuples with custom function
transform := func(t openfgaSdk.Tuple) (openfgaSdk.Tuple, error) {
    t.Object = "new:" + t.Object
    return t, nil
}
omg.TransformAllTuples(ctx, client, "document", "viewer", transform)

// Batch write tuples (100 per batch)
omg.WriteTuplesBatch(ctx, client, tuples)

// Batch delete tuples
omg.DeleteTuplesBatch(ctx, client, tuples)

Backup & Restore

// Backup all tuples before risky operation
backup, err := omg.BackupTuples(ctx, client)

// Restore if something goes wrong
omg.RestoreTuples(ctx, client, backup)

πŸ“ Project Structure

.
β”œβ”€β”€ cmd/omg/                    # CLI application
β”‚   └── main.go
β”œβ”€β”€ pkg/
β”‚   β”œβ”€β”€ client.go              # OpenFGA SDK wrapper
β”‚   β”œβ”€β”€ migration.go           # Migration registry
β”‚   β”œβ”€β”€ tracker.go             # Migration tracking
β”‚   β”œβ”€β”€ helpers.go             # Migration helper functions
β”‚   β”œβ”€β”€ model_parser.go        # DSL parser
β”‚   β”œβ”€β”€ model_tracker.go       # Change detection & confidence
β”‚   └── migration_generator.go # Code generation
β”œβ”€β”€ migrations/                # Generated/manual migrations
β”‚   β”œβ”€β”€ migrations.go
β”‚   └── YYYYMMDDHHMMSS_name.go
β”œβ”€β”€ model.fga                  # Your authorization model (desired state)
β”œβ”€β”€ .env                       # Configuration
└── README.md

πŸ§ͺ Testing

Unit Tests (No Docker Required)

go test -v ./pkg -run Unit

Integration Tests (Requires Docker)

# Start Docker first
# Then run all tests
go test ./...

# With coverage
go test -cover ./...

Integration tests use testcontainers to spin up real OpenFGA instances.

πŸ“– Best Practices

1. Model-First for Schema Changes

Use the model-first workflow for all schema changes:

# Edit model.fga
vim model.fga

# See changes
./omg diff

# Generate migration
./omg generate descriptive_name

# Review and apply
./omg up

2. Manual Migrations for Complex Operations

Use manual migrations for:

  • User ID format changes
  • Bulk data transformations
  • Complex tuple migrations
  • Data fixes and corrections

3. Trust the Confidence System

  • High confidence: Usually safe to apply directly
  • Medium confidence: Review but likely correct
  • Low confidence: Verify the rename is intentional
  • No detection: Correctly identified as separate operations

4. Always Review Generated Code

Even with high confidence, review migrations before applying:

# Review the generated file
cat migrations/YYYYMMDDHHMMSS_name.go

# Edit if needed
vim migrations/YYYYMMDDHHMMSS_name.go

# Then apply
./omg up

5. Test Locally First

# Test in local environment
OPENFGA_STORE_ID=test-store ./omg up

# Check result
./omg status

# Test rollback
./omg down

6. Commit Your Model

Commit model.fga to version control:

git add model.fga migrations/
git commit -m "Add folder type to authorization model"

This keeps your team in sync with model changes.

7. Use Descriptive Migration Names

# Good
./omg generate add_folder_hierarchy
./omg generate rename_team_to_organization

# Bad
./omg generate update
./omg generate fix

πŸ“š Migration Patterns

Pattern 1: Adding New Type

1. Edit model.fga - add new type
2. Run: omg generate add_<type>
3. Review generated code
4. Apply: omg up

Pattern 2: Renaming Type/Relation

1. Edit model.fga - change name
2. Run: omg diff (see confidence level)
3. Run: omg generate rename_<old>_to_<new>
4. Review:
   - High confidence: usually correct
   - Medium confidence: verify it's a rename
   - Low confidence: check both options
5. Apply: omg up

Pattern 3: Complex Schema + Data Change

1. Generate schema migration:
   omg generate update_schema

2. Edit generated file to add data transformations:
   vim migrations/YYYYMMDDHHMMSS_update_schema.go

3. Apply combined migration:
   omg up

Pattern 4: Backward Compatible Change

1. Migration 1: Add new relation (copy from old)
2. Deploy app update (use new relation)
3. Migration 2: Remove old relation

🌐 Environment Variables

Variable Required Default Description
OPENFGA_API_URL Yes - OpenFGA API endpoint
OPENFGA_STORE_ID Yes - OpenFGA store ID
OPENFGA_AUTH_METHOD No none Auth: none, token, or client_credentials
OPENFGA_API_TOKEN Conditional - API token (if AUTH_METHOD=token)
OPENFGA_CLIENT_ID Conditional - OAuth client ID
OPENFGA_CLIENT_SECRET Conditional - OAuth client secret
OPENFGA_TOKEN_ISSUER No - OAuth issuer URL
OPENFGA_TOKEN_AUDIENCE No - OAuth audience
LOG_LEVEL No info Log level: debug, info, warn, error

πŸ”— Related Documentation

🀝 Contributing

Contributions welcome! Please:

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for new functionality
  4. Ensure all tests pass
  5. Submit a pull request

πŸ“„ License

MIT License - see LICENSE file for details

πŸ™ Acknowledgments


Made with ❀️ for the OpenFGA community

About

Openfga MiGration Tool

Resources

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors