Skip to content

Go API

Complete reference for the github.com/pthm/melange/melange Go runtime package. This package has zero external dependencies (stdlib only).

Core Types

ObjectType

type ObjectType string

String alias for type names (e.g., "user", "repository").

Object

type Object struct {
    Type ObjectType
    ID   string
}

Represents a typed resource. Implements both ObjectLike and SubjectLike, so it can be used on either side of a permission check.

  • String() string returns "type:id"
  • FGAObject() Object implements ObjectLike
  • FGASubject() Object implements SubjectLike

Relation

type Relation string

String alias for relation names (e.g., "owner", "can_read").

  • String() string returns the relation name
  • FGARelation() Relation implements RelationLike

ContextualTuple

type ContextualTuple struct {
    Subject  Object
    Relation Relation
    Object   Object
}

A temporary tuple injected at check time. Not persisted. Requires a *sql.Tx or *sql.Conn as the Querier (not *sql.DB).

PageOptions

type PageOptions struct {
    Limit int
    After *string
}

Controls pagination for list operations. Limit of 0 or negative means no limit. After is a cursor from a previous page.

Interfaces

ObjectLike

type ObjectLike interface {
    FGAObject() Object
}

Implement this on your domain types so they can be passed directly to Check, ListObjects, etc.

SubjectLike

type SubjectLike interface {
    FGASubject() Object
}

Same as ObjectLike but for the subject side of a permission check.

RelationLike

type RelationLike interface {
    FGARelation() Relation
}

Implement on your relation types. The generated client code produces constants that implement this.

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)
}

Satisfied by *sql.DB, *sql.Tx, and *sql.Conn.

TypeContextual TuplesSees Uncommitted Data
*sql.DBNo (returns ErrContextualTuplesUnsupported)No
*sql.TxYesYes
*sql.ConnYesNo

Execer

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

Extends Querier with write capability. Required internally for contextual tuple injection.

Cache

type Cache interface {
    Get(subject Object, relation Relation, object Object) (allowed bool, err error, ok bool)
    Set(subject Object, relation Relation, object Object, allowed bool, err error)
}

Implement this interface for custom cache backends (e.g., Redis). The built-in CacheImpl satisfies this interface.

ExplainCache

type ExplainCache interface {
    GetExplain(subject Object, relation Relation, object Object, maxNodes int) (trace *Trace, err error, ok bool)
    SetExplain(subject Object, relation Relation, object Object, maxNodes int, trace *Trace, err error)
}

Opt-in extension for caching Explain traces. Checker.Explain type-asserts the injected Cache to ExplainCache and, on success, consults / populates the Explain cache before the DB call. Cache implementations that only implement Cache fall through to the DB on every Explain call — no breakage.

maxNodes is part of the key. Different WithExplainMaxNodes values produce different traces (truncation flips), so they get distinct entries.

ExpandCache

type ExpandCache interface {
    GetExpand(object Object, relation Relation, subjectType ObjectType, maxLeaf int) (tree *UsersetTree, err error, ok bool)
    SetExpand(object Object, relation Relation, subjectType ObjectType, maxLeaf int, tree *UsersetTree, err error)
}

Opt-in extension for caching Expand trees. Same contract as ExplainCache. subjectType (the WithSubjectTypeFilter value) and maxLeaf (WithExpandMaxLeaf) are both part of the key because each changes the emitted tree.

The default CacheImpl implements Cache, ExplainCache, and ExpandCache. Custom cache backends implement only the interfaces they support.

Validator

type Validator interface {
    ValidateUsersetSubject(subject Object) error
    ValidateCheckRequest(subject Object, relation Relation, object Object) error
    ValidateListUsersRequest(relation Relation, object Object, subjectType ObjectType) error
    ValidateContextualTuple(tuple ContextualTuple) error
}

Schema-aware validation. The generated client code can provide a validator; use WithValidator() to supply it.

Checker

Creating a Checker

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

Options:

OptionDescription
WithCache(c Cache)Enable caching for permission checks
WithDecision(d Decision)Set a static decision override (Allow or Deny)
WithContextDecision()Enable context-based decision overrides
WithUsersetValidation()Validate userset subjects before checking
WithRequestValidation()Validate all check requests before executing
WithValidator(v Validator)Supply a schema-aware validator
WithDatabaseSchema(s string)Set the PostgreSQL schema where melange objects live (see Custom Database Schema)

Permission Checks

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

Returns true if the subject has the relation on the object.

func (c *Checker) CheckWithContextualTuples(ctx context.Context, subject SubjectLike, relation RelationLike, object ObjectLike, tuples []ContextualTuple) (bool, error)

Same as Check but with temporary tuples injected for this call only. Requires *sql.Tx or *sql.Conn.

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

Panics if the check is denied or returns an error. Use for internal invariants, not request handling.

List Objects

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

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

func (c *Checker) ListObjectsWithContextualTuples(ctx context.Context, subject SubjectLike, relation RelationLike, objectType ObjectType, tuples []ContextualTuple, page PageOptions) (ids []string, nextCursor *string, err error)

ListObjects returns object IDs of the given type that the subject can access. ListObjectsAll auto-paginates to return all results.

List Subjects

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

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

func (c *Checker) ListSubjectsWithContextualTuples(ctx context.Context, object ObjectLike, relation RelationLike, subjectType ObjectType, tuples []ContextualTuple, page PageOptions) (ids []string, nextCursor *string, err error)

ListSubjects returns subject IDs of the given type that have the relation on the object. ListSubjectsAll auto-paginates.

Explain

func (c *Checker) Explain(ctx context.Context, subject SubjectLike, relation RelationLike, object ObjectLike, opts ...ExplainOption) (*Trace, error)

Returns the resolution tree for a check: every attempted branch, contributing tuples, per-branch success/failure. Use for debugging and admin tooling, not the request path — Explain builds a JSONB trace per call.

trace, err := checker.Explain(ctx,
    melange.Object{Type: "user", ID: "alice"},
    melange.Relation("viewer"),
    melange.Object{Type: "document", ID: "1"},
)
if err != nil {
    return err
}

if *trace.Result {
    fmt.Println("allowed via", trace.Root.Type, "—", trace.Root.Label)
}

Options

func WithExplainMaxNodes(n int) ExplainOption

Caps the node count in a single trace. Precedence: per-call > session GUC melange.max_explain_nodes > default (100). n <= 0 is a no-op (defers to the GUC, then the default). When the cap is hit, trace.Truncated is true.

Trace, Node, NodeType

Trace, Node, NodeType, TupleRef, and SubjectRef model the JSONB the SQL dispatcher returns. JSON tags are snake_case to match the SQL column names.

type Trace struct {
    Object    string
    Relation  string
    Subject   string
    Result    *bool
    Root      *Node
    Truncated bool
    NodeCount int
}

type Node struct {
    Type     NodeType
    Label    string
    Evidence []TupleRef
    Children []*Node
    Users    []SubjectRef  // NodeWildcard sentinel entry only
    Result   *bool         // per-branch outcome; nil on cycle/truncated nodes
}

See the Explaining Decisions guide for the node-type table, truncation behaviour, and the supported schema patterns.

Expand

func (c *Checker) Expand(ctx context.Context, object ObjectLike, relation RelationLike, opts ...ExpandOption) (*UsersetTree, error)

Returns the OpenFGA-shaped UsersetTree for (object, relation). Resolution is shallow: computed rewrites surface as Leaf.Computed pointers and TTUs as Leaf.TupleToUserset pointers. The caller chases pointers with follow-up Expand calls, or uses ExpandRecursive for the walker.

tree, err := checker.Expand(ctx,
    melange.Object{Type: "document", ID: "1"},
    melange.Relation("viewer"),
)
if err != nil {
    return err
}

// FlattenUsers collects every Leaf.Users entry in the returned tree
// (concrete users, inlined userset refs, wildcards) without chasing
// pointers.
for _, u := range tree.FlattenUsers() {
    fmt.Println(u)
}
func (c *Checker) ExpandRecursive(ctx context.Context, object ObjectLike, relation RelationLike, opts ...ExpandOption) ([]string, error)

Walks Leaf.Computed and Leaf.TupleToUserset pointers with additional Expand calls and returns the flat, deduplicated user-string list. Cycle-safe (every (object, relation) pair is expanded at most once per call). Wildcards and userset references survive as their string forms ("user:*", "group:eng#member"); the walker does not chase userset refs because OpenFGA models them as inline subjects, not pointers.

Cost is N round-trips for N distinct pointers. Suitable for admin / debugging flows, not the request path — for the request path use ListObjects or Check.

Options

func WithSubjectTypeFilter(subjectType ObjectType) ExpandOption

Narrows Leaf.Users entries to a single subject type. "" (the default) matches all types. Melange extension — OpenFGA Expand has no equivalent.

func WithExpandMaxLeaf(n int) ExpandOption

Caps each Leaf.Users list to n entries. n <= 0 defers to session GUC melange.max_expand_leaf, then to unbounded (matching OpenFGA). When the cap fires, the emitted Users.UsersTruncated field is true. Melange extension.

UsersetTree, UsersetTreeNode, Leaf, Difference, Nodes

Field-for-field mirror of openfgav1.UsersetTree so existing OpenFGA tooling deserialises the JSON without adapters.

type UsersetTree struct {
    Root *UsersetTreeNode
}

type UsersetTreeNode struct {
    Name         string       // "document:1#viewer"
    Leaf         *Leaf
    Difference   *Difference  // named base / subtract slots
    Union        *Nodes
    Intersection *Nodes
}

type Leaf struct {
    Users          *Users
    Computed       *Computed        // {Userset: "document:1#editor"}
    TupleToUserset *TupleToUserset  // {Tupleset: ..., Computed: [...]}
}

type Users struct {
    Users          []string  // "user:alice", "group:eng#member", "user:*"
    UsersTruncated bool      // Melange extension; set when p_max_leaf capped the list
}

type Difference struct {
    Base     *UsersetTreeNode
    Subtract *UsersetTreeNode
}

type Nodes struct {
    Nodes []*UsersetTreeNode
}

FlattenUsers collects every Leaf.Users entry in the tree into a deduplicated, sorted slice. Difference nodes are walked into Base only (the subtract side names users to exclude).

See the Expanding Permissions guide for the tree structure, shallow-by-default semantics, and CLI usage.

Bulk Check

Building a Bulk Check

func (c *Checker) NewBulkCheck(ctx context.Context) *BulkCheckBuilder
func (b *BulkCheckBuilder) Add(subject SubjectLike, relation RelationLike, object ObjectLike) *BulkCheckBuilder
func (b *BulkCheckBuilder) AddWithID(id string, subject SubjectLike, relation RelationLike, object ObjectLike) *BulkCheckBuilder
func (b *BulkCheckBuilder) AddMany(subject SubjectLike, relation RelationLike, objects ...ObjectLike) *BulkCheckBuilder
func (b *BulkCheckBuilder) WithContextualTuples(tuples ...ContextualTuple) *BulkCheckBuilder
func (b *BulkCheckBuilder) Execute() (*BulkCheckResults, error)

Add assigns an auto-generated ID (the index as a string). AddWithID lets you supply your own ID (panics on duplicate or empty ID). AddMany adds multiple objects for one subject+relation pair. All checks execute in a single SQL call.

const MaxBulkCheckSize = 10000

Maximum number of checks per bulk operation.

Reading Results

func (r *BulkCheckResults) Len() int
func (r *BulkCheckResults) Get(index int) *BulkCheckResult
func (r *BulkCheckResults) GetByID(id string) *BulkCheckResult
func (r *BulkCheckResults) All() bool
func (r *BulkCheckResults) Any() bool
func (r *BulkCheckResults) None() bool
func (r *BulkCheckResults) Results() []*BulkCheckResult
func (r *BulkCheckResults) Allowed() []*BulkCheckResult
func (r *BulkCheckResults) Denied() []*BulkCheckResult
func (r *BulkCheckResults) Errors() []error
func (r *BulkCheckResults) AllOrError() error

AllOrError returns nil if every check was allowed, or a *BulkCheckDeniedError wrapping ErrBulkCheckDenied.

func (r *BulkCheckResult) ID() string
func (r *BulkCheckResult) Index() int
func (r *BulkCheckResult) Subject() Object
func (r *BulkCheckResult) Relation() Relation
func (r *BulkCheckResult) Object() Object
func (r *BulkCheckResult) IsAllowed() bool
func (r *BulkCheckResult) Err() error

Example

results, err := checker.NewBulkCheck(ctx).
    Add(user, "can_read", repo1).
    Add(user, "can_write", repo1).
    Add(user, "can_delete", repo1).
    Execute()

if results.All() {
    // Full access
}

for _, r := range results.Denied() {
    log.Printf("denied: %s on %s", r.Relation(), r.Object())
}

Cache

func NewCache(opts ...CacheOption) *CacheImpl
func WithTTL(ttl time.Duration) CacheOption
func (c *CacheImpl) Get(subject Object, relation Relation, object Object) (bool, error, bool)
func (c *CacheImpl) Set(subject Object, relation Relation, object Object, allowed bool, err error)

func (c *CacheImpl) GetExplain(subject Object, relation Relation, object Object, maxNodes int) (*Trace, error, bool)
func (c *CacheImpl) SetExplain(subject Object, relation Relation, object Object, maxNodes int, trace *Trace, err error)

func (c *CacheImpl) GetExpand(object Object, relation Relation, subjectType ObjectType, maxLeaf int) (*UsersetTree, error, bool)
func (c *CacheImpl) SetExpand(object Object, relation Relation, subjectType ObjectType, maxLeaf int, tree *UsersetTree, err error)

func (c *CacheImpl) Size() int
func (c *CacheImpl) Clear()

The built-in cache is an in-memory map, thread-safe, unbounded within the TTL window. Entries expire individually based on their insertion time. Size returns the total count across Check, Explain, and Expand families; Clear resets all three.

CacheImpl implements Cache, ExplainCache, and ExpandCache — one WithCache call activates caching for every API.

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

Decision Overrides

const (
    DecisionUnset Decision = iota // No override, perform normal check
    DecisionAllow                  // Always allow
    DecisionDeny                   // Always deny
)

Checker-level override (applies to all checks):

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

Context-level override (per-request):

checker := melange.NewChecker(db, melange.WithContextDecision())

ctx := melange.WithDecisionContext(ctx, melange.DecisionAllow)
allowed, _ := checker.Check(ctx, subject, relation, object) // always true
func WithDecisionContext(ctx context.Context, decision Decision) context.Context
func GetDecisionContext(ctx context.Context) Decision

Errors

See Errors Reference for the full error type and code reference.

Next Steps