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.
- Automatic migration generation from
model.fgachanges - 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
- 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)
- 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
# 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/omgCreate a .env file or set environment variables:
OPENFGA_API_URL=http://localhost:8080
OPENFGA_STORE_ID=your-store-id
OPENFGA_AUTH_METHOD=noneFor production with authentication:
OPENFGA_API_URL=https://api.fga.example
OPENFGA_STORE_ID=01HXYZ...
OPENFGA_AUTH_METHOD=token
OPENFGA_API_TOKEN=your-api-tokenCreate or edit model.fga:
model
schema 1.1
type user
type document
relations
define owner: [user]
define editor: [user]
define viewer: [user] or editor
./omg diffOutput:
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
./omg generate initial_modelOutput:
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
# Review the generated code
cat migrations/20241128150000_initial_model.go
# Apply the migration
./omg upScenario: Rename document β documents (very similar names)
# Edit model.fga: change 'type document' to 'type documents'
vim model.fga
# Check changes
./omg diffOutput:
β Rename detected: 'document' -> 'documents' (high confidence: 88% name, 100% relations)
Old: document
New: documents
# Generate migration
./omg generate pluralize_documentsGenerated 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
Scenario: Rename document β file with same relations
# Edit model.fga: change 'type document' to 'type file'
vim model.fga
./omg diffOutput:
β Possible rename: 'document' -> 'file' (medium confidence - review required)
Old: document
New: file
./omg generate rename_to_fileGenerated 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)
}Scenario: Very different names or no relation similarity
./omg diffOutput:
- Type 'document' removed
+ New type 'asset' with 3 relations
./omg generate separate_typesGenerated 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
Compare model.fga with current state:
./omg diffGenerate migration from detected changes:
./omg generate add_foldersInitialize tracking for a store:
./omg init my-storeApply all pending migrations:
./omg upRollback the last migration:
./omg downShow migration status:
./omg statusOutput:
Migration Status:
================
20241128150000 initial_model APPLIED
20241128151000 add_folders PENDING
Total: 2 migrations (1 applied, 1 pending)
Display current authorization model:
./omg show-modelList tuples, optionally filtered by type:
./omg list-tuples
./omg list-tuples documentList available OpenFGA stores:
./omg list-storesFor complex data operations that can't be auto-generated, create manual migrations:
./omg create custom_data_migrationpackage 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)
}OMG uses multi-factor analysis to determine rename confidence:
-
Name Similarity (Levenshtein distance)
teamβteams: 80% similardocumentβfile: 30% similarteamβorganization: 8% similar
-
Relation Structure (Jaccard coefficient)
- Same relations: 100% similar
- Partial overlap: 50-99% similar
- No overlap: 0% similar
| 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 |
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
The generated migrations use these helper functions (you can use them in manual migrations too):
// 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")// 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")// 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 all tuples before risky operation
backup, err := omg.BackupTuples(ctx, client)
// Restore if something goes wrong
omg.RestoreTuples(ctx, client, backup).
βββ 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
go test -v ./pkg -run Unit# Start Docker first
# Then run all tests
go test ./...
# With coverage
go test -cover ./...Integration tests use testcontainers to spin up real OpenFGA instances.
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 upUse manual migrations for:
- User ID format changes
- Bulk data transformations
- Complex tuple migrations
- Data fixes and corrections
- 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
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# Test in local environment
OPENFGA_STORE_ID=test-store ./omg up
# Check result
./omg status
# Test rollback
./omg downCommit 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.
# Good
./omg generate add_folder_hierarchy
./omg generate rename_team_to_organization
# Bad
./omg generate update
./omg generate fix1. Edit model.fga - add new type
2. Run: omg generate add_<type>
3. Review generated code
4. Apply: omg up
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
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
1. Migration 1: Add new relation (copy from old)
2. Deploy app update (use new relation)
3. Migration 2: Remove old relation
| 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 |
- MODEL_FIRST_GUIDE.md - Detailed model-first workflow guide
- TESTING.md - Testing guide and best practices
- CONTRIBUTING.md - Development guide
- CLAUDE.md - Complete development journal
Contributions welcome! Please:
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
MIT License - see LICENSE file for details
- Inspired by Prisma, Entity Framework, and goose
- Built with OpenFGA Go SDK
- Testing with testcontainers-go
Made with β€οΈ for the OpenFGA community