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 stringString 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() stringreturns"type:id"FGAObject() ObjectimplementsObjectLikeFGASubject() ObjectimplementsSubjectLike
Relation
type Relation stringString alias for relation names (e.g., "owner", "can_read").
String() stringreturns the relation nameFGARelation() RelationimplementsRelationLike
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.
| Type | Contextual Tuples | Sees Uncommitted Data |
|---|---|---|
*sql.DB | No (returns ErrContextualTuplesUnsupported) | No |
*sql.Tx | Yes | Yes |
*sql.Conn | Yes | No |
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) *CheckerOptions:
| Option | Description |
|---|---|
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) ExplainOptionCaps 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) ExpandOptionNarrows Leaf.Users entries to a single subject type. "" (the default) matches all types. Melange extension — OpenFGA Expand has no equivalent.
func WithExpandMaxLeaf(n int) ExpandOptionCaps 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) *BulkCheckBuilderfunc (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 = 10000Maximum 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() errorAllOrError 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() errorExample
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) CacheOptionfunc (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 truefunc WithDecisionContext(ctx context.Context, decision Decision) context.Context
func GetDecisionContext(ctx context.Context) DecisionErrors
See Errors Reference for the full error type and code reference.
Next Steps
- Checking Permissions: usage patterns, caching, transactions
- Errors Reference: sentinel errors, validation codes, error helpers
- SQL API: calling permission functions directly from SQL
- Generated Code: what
melange generate clientproduces