melange

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 3, 2026 License: MIT Imports: 13 Imported by: 0

README

Melange

Melange is a pure PostgreSQL + Go authorization library inspired by OpenFGA/Zanzibar and the rover-app pgfga implementation: https://github.com/rover-app/pgfga

Overview

Melange provides fine-grained authorization with:

  • PostgreSQL functions for permission checks
  • Zero tuple sync (permissions derived from a view over your tables)
  • Optional code generation for type-safe constants
  • Zero runtime dependencies (core library is pure stdlib)

Module Structure

Melange is split into two modules for clean dependency isolation:

Module Purpose Dependencies
github.com/pthm/melange Core runtime (checker, types, errors) stdlib only
github.com/pthm/melange/tooling Schema parsing, CLI, migration helpers OpenFGA parser

Most applications only import the core module at runtime. The tooling module is used during development (CLI, code generation) or if you need programmatic schema parsing.

Requirements

  • PostgreSQL database
  • A .fga schema file (parsed by CLI or tooling module)
  • A melange_tuples view that maps your domain tables into tuples

Quick Start

  1. Create a schema file (schema.fga):
model
  schema 1.1

type user

type repository
  relations
    define owner: [user]
    define can_read: owner
  1. Create a melange_tuples view:
CREATE OR REPLACE VIEW melange_tuples AS
SELECT
    'user' AS subject_type,
    user_id::text AS subject_id,
    'owner' AS relation,
    'repository' AS object_type,
    repository_id::text AS object_id
FROM repository_owners;
  1. Apply Melange infrastructure + schema:
melange migrate --db postgres://localhost/mydb --schemas-dir schemas
  1. Generate type-safe Go constants:
melange generate --schemas-dir schemas --generate-dir internal/authz --generate-pkg authz
  1. Check permissions in Go:
checker := melange.NewChecker(db)
ok, err := checker.Check(ctx, authz.User("123"), authz.RelCanRead, authz.Repository("456"))
if err != nil {
    return err
}
if !ok {
    return ErrForbidden
}

Core Concepts

  • Objects: Both subjects and resources are modeled as objects.
    • Object{Type: "user", ID: "123"}
  • Relations: Simple strings (generated constants are optional).
  • Wildcard: Use * as a subject ID for public access (type:*).

Checker API

Melange works with *sql.DB, *sql.Tx, or *sql.Conn.

checker := melange.NewChecker(db)
ok, err := checker.Check(ctx, subject, relation, object)
ids, err := checker.ListObjects(ctx, subject, relation, objectType)

Caching

cache := melange.NewCache(melange.WithTTL(time.Minute))
checker := melange.NewChecker(db, melange.WithCache(cache))

Decision Overrides

For tests or admin tools:

checker := melange.NewChecker(db, melange.WithDecision(melange.DecisionAllow))

Error Handling

Sentinel errors:

  • melange.ErrNoTuplesTable - melange_tuples view doesn't exist
  • melange.ErrMissingModel - melange_model table doesn't exist
  • melange.ErrEmptyModel - Model table exists but is empty
  • melange.ErrInvalidSchema - Schema parsing failed
  • melange.ErrMissingFunction - SQL functions not installed

Helpers:

  • melange.IsNoTuplesTableErr(err)
  • melange.IsMissingModelErr(err)
  • melange.IsEmptyModelErr(err)
  • melange.IsInvalidSchemaErr(err)
  • melange.IsMissingFunctionErr(err)

Schema Helpers

Query schema definitions to build dynamic UIs or introspect the model:

types := []melange.TypeDefinition{...} // from tooling.ParseSchema

// Get all unique subject types across the schema
subjects := melange.SubjectTypes(types)
// e.g., ["user", "team", "organization"]

// Get subject types for a specific relation
allowed := melange.RelationSubjects(types, "repository", "owner")
// e.g., ["user"]  (only users can be owners)

Programmatic Migration

For programmatic schema loading (without the CLI):

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

// Parse and migrate in one step
err := tooling.Migrate(ctx, db, "schemas")

// Or with more control:
types, err := tooling.ParseSchema("schemas/schema.fga")
migrator := melange.NewMigrator(db, "schemas")
err = migrator.MigrateWithTypes(ctx, types)

The Migrator also supports individual steps:

migrator := melange.NewMigrator(db, "schemas")

// Apply DDL only (tables + functions)
err := migrator.ApplyDDL(ctx)

// Load schema into model table
err := migrator.MigrateWithTypes(ctx, types)

// Check current status
status, err := migrator.GetStatus(ctx)
// status.SchemaExists, status.ModelCount

CLI

melange [command] [flags]

Commands:
  migrate   Apply schema to database
  generate  Generate Go types from schema
  validate  Validate schema syntax
  status    Show current schema status

Documentation

Overview

Package melange provides PostgreSQL-based fine-grained authorization implementing OpenFGA/Zanzibar concepts with zero runtime dependencies.

Module Structure

Melange is split into two modules for clean dependency isolation:

  • github.com/pthm/melange (core): Runtime checker, types, errors. Stdlib only.
  • github.com/pthm/melange/tooling: Schema parsing, migration helpers. Depends on OpenFGA parser.

Most applications import only the core module at runtime. The tooling module is used during development (CLI, code generation) or for programmatic schema parsing.

Zero Tuple Sync

Melange uses a pure-PostgreSQL approach where permissions are derived from views over your existing application tables rather than maintaining separate tuple storage. You define a melange_tuples view over tables like users, repositories, etc. Permission checks query this view combined with the authorization model stored in melange_model.

Core Concepts

Objects represent typed resources. In FGA terms, both "users" and "resources" are objects - there's no special Subject type.

user := melange.Object{Type: "user", ID: "123"}
repo := melange.Object{Type: "repository", ID: "456"}

Basic Usage

checker := melange.NewChecker(db)
ok, err := checker.Check(ctx, user, "can_read", repo)

Transaction Support

The Checker works with *sql.DB, *sql.Tx, or *sql.Conn, enabling permission checks to see uncommitted changes within a transaction:

tx, _ := db.BeginTx(ctx, nil)
checker := melange.NewChecker(tx)
ok, _ := checker.Check(ctx, user, "can_write", repo)
// Permission check sees uncommitted transaction state

Caching

Use WithCache for repeated checks:

cache := melange.NewCache(melange.WithTTL(time.Minute))
checker := melange.NewChecker(db, melange.WithCache(cache))

Decision Overrides

Use WithDecision for testing or admin tools:

checker := melange.NewChecker(db, melange.WithDecision(melange.DecisionAllow))

Schema Management

For schema parsing and migration, use the tooling module:

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

types, _ := tooling.ParseSchema("schemas/schema.fga")
err := tooling.Migrate(ctx, db, "schemas")

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrNoTuplesTable is returned when the melange_tuples view doesn't exist.
	// This typically means the application hasn't created the view over its
	// domain tables. See the melange documentation for view creation examples.
	ErrNoTuplesTable = errors.New("melange: melange_tuples view/table not found")

	// ErrMissingModel is returned when the melange_model table doesn't exist.
	// Run `melange migrate` to create the table and load the FGA schema.
	ErrMissingModel = errors.New("melange: melange_model table missing")

	// ErrEmptyModel is returned when the melange_model table exists but is empty.
	// This means the schema hasn't been loaded. Run `melange migrate` to
	// parse the .fga file and populate the model.
	ErrEmptyModel = errors.New("melange: authorization model empty")

	// ErrInvalidSchema is returned when schema parsing fails.
	// Check the .fga file syntax using `fga model validate` from the OpenFGA CLI.
	ErrInvalidSchema = errors.New("melange: invalid schema")

	// ErrMissingFunction is returned when a required PostgreSQL function doesn't exist.
	// Run `melange migrate` to create the check_permission and list_accessible_* functions.
	ErrMissingFunction = errors.New("melange: authorization function missing")
)

Sentinel errors for common failure modes during permission checks. These errors indicate setup issues, not permission denials. Permission checks return (false, nil) for denied access. These errors mean the authorization system cannot function due to missing schema components.

Use the Is*Err helper functions to check for specific errors and provide helpful setup messages to users.

Functions

func GenerateGo

func GenerateGo(w io.Writer, types []TypeDefinition, cfg *GenerateConfig) error

GenerateGo writes Go code for the types and relations from a parsed schema. Unlike some FGA implementations, this generates constants for ALL relations, not just those with "can_" prefix (unless filtered via config).

The generated code includes:

  • ObjectType constants (TypeUser, TypeRepository, etc.)
  • Relation constants (RelCanRead, RelOwner, etc.)
  • Constructor functions (User(id), Repository(id), etc.)
  • Wildcard constructors (AnyUser(), AnyRepository(), etc.)

The constructors return melange.Object values, enabling type-safe permission checks:

checker.Check(ctx, authz.User(123), authz.RelCanRead, authz.Repository(456))

Wildcard constructors support public access patterns:

// Grant all users permission
INSERT INTO tuples (subject_type, subject_id, relation, object_type, object_id)
VALUES ('user', '*', 'can_read', 'repository', '456')

checker.Check(ctx, authz.AnyUser(), authz.RelCanRead, authz.Repository(456))

func IsEmptyModelErr

func IsEmptyModelErr(err error) bool

IsEmptyModelErr returns true if err is or wraps ErrEmptyModel.

func IsInvalidSchemaErr

func IsInvalidSchemaErr(err error) bool

IsInvalidSchemaErr returns true if err is or wraps ErrInvalidSchema.

func IsMissingFunctionErr

func IsMissingFunctionErr(err error) bool

IsMissingFunctionErr returns true if err is or wraps ErrMissingFunction.

func IsMissingModelErr

func IsMissingModelErr(err error) bool

IsMissingModelErr returns true if err is or wraps ErrMissingModel.

func IsNoTuplesTableErr

func IsNoTuplesTableErr(err error) bool

IsNoTuplesTableErr returns true if err is or wraps ErrNoTuplesTable.

func RelationSubjects

func RelationSubjects(types []TypeDefinition, objectType string, relation string) []string

RelationSubjects returns the subject types that can have a specific relation on objects of the given type. This is useful for understanding who can be granted a particular permission.

Example:

types, _ := melange.ParseSchema("schema.fga")
subjects := melange.RelationSubjects(types, "repository", "owner")
// Returns: ["user"] - only users can be repository owners

readers := melange.RelationSubjects(types, "repository", "can_read")
// Returns: ["user", "organization"] - users and orgs can read repositories

func SubjectTypes

func SubjectTypes(types []TypeDefinition) []string

SubjectTypes returns all types that can be subjects in authorization checks. A type is a subject type if it appears in any relation's SubjectTypes list. This is useful for understanding which types can be the "who" in permission checks.

Example:

types, _ := melange.ParseSchema("schema.fga")
subjects := melange.SubjectTypes(types)
// Returns: ["user", "organization", "team"]

func WithDecisionContext

func WithDecisionContext(ctx context.Context, decision Decision) context.Context

WithDecisionContext returns a new context with the given decision. This allows decision overrides to flow through context rather than requiring explicit Checker construction.

Prefer WithDecision option for explicit control. Use context-based decisions when the override needs to propagate through multiple layers where passing a Checker instance is impractical.

Note: The Checker does NOT automatically consult this context value. This is a utility for applications that want to propagate authorization decisions through their own middleware or handler chains.

Types

type AuthzModel

type AuthzModel struct {
	ID               int64
	ObjectType       string  // Object type this rule applies to
	Relation         string  // Relation this rule defines
	SubjectType      *string // Allowed subject type (for direct rules)
	ImpliedBy        *string // Implying relation (for role hierarchy)
	ParentRelation   *string // Parent relation to check (for inheritance)
	ExcludedRelation *string // Relation to exclude (for "but not" rules)
}

AuthzModel represents an entry in the melange_model table. Each row defines one authorization rule that check_permission evaluates.

The table stores the flattened authorization model, precomputing transitive closures and normalizing rules for efficient query execution.

Rule types:

  • Direct: SubjectType is set, others NULL (user can have relation)
  • Implied: ImpliedBy is set (having one relation grants another)
  • Parent: ParentRelation and SubjectType set (inherit from parent object)
  • Exclusive: ExcludedRelation set (permission denied if exclusion holds)

func ToAuthzModels

func ToAuthzModels(types []TypeDefinition) []AuthzModel

ToAuthzModels converts parsed type definitions to database models. This is the critical transformation that enables permission checking.

The conversion performs transitive closure of implied_by relationships to support role hierarchies. For example, if owner → admin and admin → member, the closure ensures owner also implies member without explicit declaration.

Each AuthzModel row represents one authorization rule:

  • Direct subject types: "repository.can_read allows user"
  • Implied relations: "repository.can_read implied by can_write"
  • Parent inheritance: "change.can_read from repository.can_read"
  • Exclusions: "change.can_read but not is_author"

The check_permission function queries these rows to evaluate permissions recursively, following the graph of implications and parent relationships.

type Cache

type Cache interface {
	// Get retrieves a cached permission check result.
	// Returns (allowed, err, found). If found is false, the entry doesn't exist or is expired.
	Get(subject Object, relation Relation, object Object) (allowed bool, err error, ok bool)

	// Set stores a permission check result in the cache.
	Set(subject Object, relation Relation, object Object, allowed bool, err error)
}

Cache stores permission check results. It is safe for concurrent use from multiple goroutines.

Implementations should cache both allowed and denied permissions, including errors. This reduces database load for repeated checks of denied access.

type CacheImpl

type CacheImpl struct {
	// contains filtered or unexported fields
}

CacheImpl is the default in-memory cache implementation with optional TTL. It uses a sync.RWMutex for goroutine safety. For high-contention scenarios, consider a sharded cache or external cache (Redis, etc.).

The cache grows unbounded within its TTL window. For long-running processes with large permission sets, consider periodic clearing or TTL-based expiry.

func NewCache

func NewCache(opts ...CacheOption) *CacheImpl

NewCache creates a new permission cache. The cache is safe for concurrent use but scoped to a single process. For distributed systems, implement Cache with a distributed store.

func (*CacheImpl) Clear

func (c *CacheImpl) Clear()

Clear removes all entries from the cache. Useful for testing or when permission data changes globally (e.g., after schema migration or mass permission updates).

func (*CacheImpl) Get

func (c *CacheImpl) Get(subject Object, relation Relation, object Object) (bool, error, bool)

Get retrieves a cached permission check result. Returns (allowed, err, found). If found is false, the entry doesn't exist or is expired.

func (*CacheImpl) Set

func (c *CacheImpl) Set(subject Object, relation Relation, object Object, allowed bool, err error)

Set stores a permission check result in the cache.

func (*CacheImpl) Size

func (c *CacheImpl) Size() int

Size returns the number of entries in the cache. Useful for monitoring cache growth and memory usage.

type CacheOption

type CacheOption func(*CacheImpl)

CacheOption configures a Cache.

func WithTTL

func WithTTL(ttl time.Duration) CacheOption

WithTTL sets the time-to-live for cache entries. Entries older than TTL are considered stale and will be re-checked. A TTL of 0 (default) means entries never expire within the cache's lifetime.

Choose TTL based on permission volatility:

  • Short TTL (seconds): Frequently changing permissions, high security
  • Medium TTL (minutes): Typical web applications
  • Long TTL or none: Near-static permissions, performance-critical paths

type Checker

type Checker struct {
	// contains filtered or unexported fields
}

Checker performs authorization checks against PostgreSQL. It evaluates permissions using the melange_model table (parsed FGA schema) and the melange_tuples view (application data).

Checkers are lightweight and safe to create per-request. They hold no state beyond the database handle, cache, and decision override. The database handle can be *sql.DB, *sql.Tx, or *sql.Conn, allowing permission checks to see uncommitted changes within transactions.

Schema validation runs once per process on the first NewChecker call with a non-nil Querier. Validation issues are logged as warnings but do not prevent Checker creation, allowing applications to start even if the authorization schema is not yet fully configured.

func NewChecker

func NewChecker(q Querier, opts ...Option) *Checker

NewChecker creates a checker that works with *sql.DB, *sql.Tx, or *sql.Conn. Options allow callers to enable caching or decision overrides.

The Querier interface is satisfied by all three database handle types, enabling checkers to work seamlessly in transaction or connection-pooled contexts without requiring different APIs.

On the first call with a non-nil Querier, NewChecker validates the schema (once per process). Validation issues are logged as warnings but do not prevent Checker creation. This allows applications to start even if the authorization schema is not yet fully configured.

func (*Checker) Check

func (c *Checker) Check(ctx context.Context, subject SubjectLike, relation RelationLike, object ObjectLike) (bool, error)

Check returns true if subject has the relation on object. The check evaluates direct tuples, implied relations (role hierarchies), and parent inheritance according to the loaded FGA schema.

Example:

ok, err := checker.Check(ctx, authz.User("123"), authz.RelCanRead, authz.Repository("456"))

If a cache is configured via WithCache, results are cached by the tuple (subject, relation, object). The cache stores both successful and failed checks, including errors. This prevents repeated database queries for denied permissions or missing objects.

If a decision override is set via WithDecision, the database is not queried. If WithContextDecision is enabled, context decisions are also consulted.

func (*Checker) ListObjects

func (c *Checker) ListObjects(ctx context.Context, subject SubjectLike, relation RelationLike, objectType ObjectType) ([]string, error)

ListObjects returns all object IDs of the given type that subject has relation on.

Example:

ids, _ := checker.ListObjects(ctx, authz.User("123"), authz.RelCanRead, authz.TypeRepository)

Note: This method does NOT use the permission cache because it returns a list rather than a single boolean result.

Note on decision overrides:

  • DecisionDeny: returns empty list (no access)
  • DecisionAllow: falls through to normal check (can't enumerate "all" objects)

func (*Checker) ListSubjects

func (c *Checker) ListSubjects(ctx context.Context, object ObjectLike, relation RelationLike, subjectType ObjectType) ([]string, error)

ListSubjects returns all subject IDs of the given type that have relation on object. This is the inverse of ListObjects - it answers "who has access to this object?"

Example:

ids, _ := checker.ListSubjects(ctx, authz.Repository("456"), authz.RelCanRead, authz.TypeUser)
// Returns IDs of all users who can read repository 456

Note: This method does NOT use the permission cache because it returns a list rather than a single boolean result.

Note on decision overrides:

  • DecisionDeny: returns empty list (no subjects have access)
  • DecisionAllow: falls through to normal check (can't enumerate "all" subjects)

func (*Checker) Must

func (c *Checker) Must(ctx context.Context, subject SubjectLike, relation RelationLike, object ObjectLike)

Must panics if the permission check fails or errors. Use in handlers after authentication has already verified the user exists.

This is useful for enforcing permissions in code paths where denial should be a programmer error rather than a user-facing error. For example:

// After RequireAuth middleware ensures user is authenticated:
repo := getRepository(...)
checker.Must(ctx, authz.User(user.ID), authz.RelCanWrite, repo)
// Only reachable if permission granted

Prefer Check() for user-facing authorization where you need to return a 403 Forbidden response. Use Must() for internal invariants where unauthorized access indicates a bug in the calling code.

type Decision

type Decision int

Decision allows bypassing DB checks for admin tools and tests. Decisions are set at Checker construction time via WithDecision, making the bypass explicit and visible in code.

const (
	// DecisionUnset means no override - perform normal permission check.
	DecisionUnset Decision = iota

	// DecisionAllow bypasses checks and always returns true (allowed).
	// Use for admin tools, background jobs, or testing authorized code paths.
	DecisionAllow

	// DecisionDeny bypasses checks and always returns false (denied).
	// Use for testing unauthorized code paths without database setup.
	DecisionDeny
)

func GetDecisionContext

func GetDecisionContext(ctx context.Context) Decision

GetDecisionContext retrieves the decision from context. Returns DecisionUnset if no decision is set.

Applications can use this to check for decision overrides before creating a Checker, enabling context-based bypass patterns.

type Execer

type Execer interface {
	Querier
	ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
}

Execer extends Querier with ExecContext for migrations. Only required by the CLI migrate command, not for runtime permission checks. Separating this interface keeps the Checker dependency minimal.

type GenerateConfig

type GenerateConfig struct {
	// Package name for generated code. Default: "authz"
	Package string

	// RelationPrefixFilter is a prefix filter for relation names.
	// Only relations with this prefix will have constants generated.
	// If empty, all relations will be generated.
	//
	// Example: "can_" generates only permission relations, omitting roles.
	RelationPrefixFilter string

	// IDType specifies the type to use for object IDs.
	// Default: "string"
	// The type must implement fmt.Stringer or be safely convertible to string.
	//
	// Common values: "string", "int64", "uuid.UUID"
	// The generated code uses fmt.Sprint(id) for conversion.
	IDType string
}

GenerateConfig configures code generation. The generator produces Go code with type-safe constants for object types and relations defined in the FGA schema.

func DefaultGenerateConfig

func DefaultGenerateConfig() *GenerateConfig

DefaultGenerateConfig returns sensible defaults. Package: "authz", no relation filter (all relations), string IDs.

type Migrator

type Migrator struct {
	// contains filtered or unexported fields
}

Migrator handles loading authorization schemas into PostgreSQL. The migrator is idempotent - safe to run on every application startup.

The migration process:

  1. Creates melange_model table (if not exists)
  2. Creates/replaces check_permission and list_accessible_* functions
  3. Loads pre-parsed authorization rules into the database

Usage with Tooling Module

For most use cases, use the tooling module's convenience functions which handle parsing and migration in one step:

import "github.com/pthm/melange/tooling"
err := tooling.Migrate(ctx, db, "schemas")

Use the core Migrator directly when you have pre-parsed TypeDefinitions or need fine-grained control (DDL-only, status checks, etc.):

types, _ := tooling.ParseSchema("schemas/schema.fga")
migrator := melange.NewMigrator(db, "schemas")
err := migrator.MigrateWithTypes(ctx, types)

This separation keeps the core melange package free of OpenFGA dependencies.

func NewMigrator

func NewMigrator(db Execer, schemasDir string) *Migrator

NewMigrator creates a new schema migrator. The schemasDir should contain a schema.fga file in OpenFGA DSL format. The Execer is typically *sql.DB but can be *sql.Tx for testing.

func (*Migrator) ApplyDDL

func (m *Migrator) ApplyDDL(ctx context.Context) error

ApplyDDL applies the melange_model table and functions. This is idempotent (CREATE TABLE IF NOT EXISTS, CREATE OR REPLACE FUNCTION).

The DDL creates:

  • melange_model table (stores parsed FGA schema)
  • check_permission function (evaluates permissions)
  • list_accessible_objects function (reverse lookup)
  • has_tuple function (direct tuple checks)

This can be called independently of schema migration to update function implementations without reloading the authorization model.

func (*Migrator) GetStatus

func (m *Migrator) GetStatus(ctx context.Context) (*Status, error)

GetStatus returns the current migration status. Useful for health checks or migration diagnostics.

func (*Migrator) HasSchema

func (m *Migrator) HasSchema() bool

HasSchema returns true if the schema file exists. Use this to conditionally run migration or skip if not configured.

func (*Migrator) MigrateWithTypes

func (m *Migrator) MigrateWithTypes(ctx context.Context, types []TypeDefinition) error

MigrateWithTypes performs database migration using pre-parsed type definitions. This is the core migration method used by the tooling package's Migrate function.

The method:

  1. Applies DDL (creates tables and functions)
  2. Converts type definitions to authorization models
  3. Truncates and repopulates melange_model with the new rules

This is idempotent - safe to run multiple times with the same types.

Uses a transaction if the db supports it (*sql.DB). This ensures the schema is updated atomically or not at all.

func (*Migrator) SchemaPath

func (m *Migrator) SchemaPath() string

SchemaPath returns the path to the schema.fga file. Conventionally named schema.fga by OpenFGA tooling.

type Object

type Object struct {
	Type ObjectType
	ID   string
}

Object represents a typed resource identifier. In FGA terms, both "users" and "resources" are objects - there's no distinction between subjects and objects at the type level.

Objects are value types and safe to copy. The canonical string format is "type:id", used in logging and debugging.

func (Object) FGAObject

func (o Object) FGAObject() Object

FGAObject returns the object itself, implementing ObjectLike. This allows Object to be used directly in permission checks.

func (Object) FGASubject

func (o Object) FGASubject() Object

FGASubject returns the object itself, implementing SubjectLike. In FGA terms, subjects are also objects - this allows Object to be used as either the subject or object in permission checks.

func (Object) String

func (o Object) String() string

String returns the canonical representation "type:id".

type ObjectLike

type ObjectLike interface {
	FGAObject() Object
}

ObjectLike defines an interface for types that can be converted to Objects. This allows domain models to implement authorization-aware methods without importing the full domain layer into melange.

Example:

type Repository struct { ID int64; OwnerName string }
func (r Repository) FGAObject() melange.Object {
    return melange.Object{Type: "repository", ID: fmt.Sprint(r.ID)}
}

The Checker accepts ObjectLike rather than Object directly, enabling type-safe authorization checks against domain models.

type ObjectType

type ObjectType string

ObjectType represents the type of an object.

func (ObjectType) String

func (ot ObjectType) String() string

String returns the string representation of the object type.

type Option

type Option func(*Checker)

Option configures a Checker.

func WithCache

func WithCache(c Cache) Option

WithCache enables caching for permission check results. Caching is safe across goroutines but scoped to a single Checker instance. For request-scoped caching, create a new Checker per request with a request-scoped cache.

func WithContextDecision

func WithContextDecision() Option

WithContextDecision enables context-based decision overrides. When enabled, Check will consult GetDecisionContext(ctx) before performing database checks. This allows authorization decisions to propagate through middleware layers.

Decision precedence when enabled:

  1. Context decision (via WithDecisionContext)
  2. Checker decision (via WithDecision)
  3. Database check

By default, context decisions are NOT consulted. This opt-in design ensures explicit control over when context can override authorization.

func WithDecision

func WithDecision(d Decision) Option

WithDecision sets a decision override that bypasses database checks. Use DecisionAllow for admin tools or testing authorized paths. Use DecisionDeny for testing unauthorized paths. This is intentionally separate from context-based overrides to make the bypass explicit at Checker construction time.

type Querier

type Querier interface {
	QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
	QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
}

Querier executes queries against PostgreSQL. Implemented by *sql.DB, *sql.Tx, and *sql.Conn.

The minimal interface allows Checker to work in transaction contexts without requiring a full database connection. This enables permission checks to see uncommitted changes within a transaction, supporting patterns like:

tx.Exec("INSERT INTO repositories ...")
// melange_tuples view reflects the new row
checker.Check(ctx, user, "can_read", repo) // sees new tuple
tx.Commit()

type Relation

type Relation string

Relation represents a typed relation identifier. Relations can be permissions (can_read, can_write) or roles (owner, member). Unlike some authorization systems, melange treats all relations uniformly.

func (Relation) FGARelation

func (r Relation) FGARelation() Relation

FGARelation returns the relation itself, implementing RelationLike.

func (Relation) String

func (r Relation) String() string

String returns the canonical representation of the relation.

type RelationDefinition

type RelationDefinition struct {
	Name             string   // Relation name: "owner", "can_read", etc.
	SubjectTypes     []string // Direct subject types: ["user"], ["organization"]
	ImpliedBy        []string // Relations that imply this one: ["owner", "admin"]
	ParentRelation   string   // For inheritance: "can_read from org" → "can_read"
	ParentType       string   // The relation linking to parent: "org", "repo"
	ExcludedRelation string   // For exclusions: "can_read but not author" → "author"
}

RelationDefinition represents a parsed relation. Relations describe who can have what relationship with an object.

A relation can be:

  • Direct: explicitly granted via tuples (SubjectTypes)
  • Implied: granted by having another relation (ImpliedBy)
  • Inherited: derived from a parent object (ParentRelation, ParentType)
  • Exclusive: granted except for excluded subjects (ExcludedRelation)

type RelationLike

type RelationLike interface {
	FGARelation() Relation
}

RelationLike defines an interface for types that can be converted to Relations. This allows generated code to provide type-safe relation constants while accepting custom relation types from domain models.

type Status

type Status struct {
	// SchemaExists indicates if the schema.fga file exists on disk.
	SchemaExists bool

	// ModelCount is the number of rows in the melange_model table.
	// Zero means the schema hasn't been loaded (run `melange migrate`).
	ModelCount int64
}

Status represents the current migration state. Use GetStatus to check if the authorization system is properly configured.

type SubjectLike

type SubjectLike interface {
	FGASubject() Object
}

SubjectLike defines an interface for types that can be used as subjects. In FGA terms, subjects are the "who" in "who has what relation on what object". Subjects are typically users but can be any typed resource.

Example:

type User struct { ID int64; Username string }
func (u User) FGASubject() melange.Object {
    return melange.Object{Type: "user", ID: fmt.Sprint(u.ID)}
}

Note: Object implements both SubjectLike and ObjectLike, allowing melange.Object values to be used directly in either position.

type TypeDefinition

type TypeDefinition struct {
	Name      string
	Relations []RelationDefinition
}

TypeDefinition represents a parsed type from an .fga file. Each type definition describes an object type (user, repository, etc.) and the relations that can exist on objects of that type.

Directories

Path Synopsis
cmd
melange module
melange module
Package sql provides embedded SQL files for Melange infrastructure.
Package sql provides embedded SQL files for Melange infrastructure.
tooling module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL