Contextual Tuples
Contextual tuples are temporary tuples injected at check time. They are not persisted to the database and only affect the single check or list call they are passed to.
When to Use
Contextual tuples are for authorization data that exists at request time but is not stored in your domain tables:
- IP-based access: inject an
ip_allowedtuple based on the client’s IP address. - Time-based access: inject a
within_windowtuple based on the current time. - Feature flags: inject a
feature_enabledtuple based on a feature flag service. - Request context: inject tuples derived from headers, tokens, or session data.
Go API
tuples := []melange.ContextualTuple{
{
Subject: melange.Object{Type: "user", ID: "alice"},
Relation: melange.Relation("ip_allowed"),
Object: melange.Object{Type: "network", ID: "office"},
},
}
allowed, err := checker.CheckWithContextualTuples(ctx, user, "can_access", resource, tuples)List operations also support contextual tuples:
ids, cursor, err := checker.ListObjectsWithContextualTuples(ctx, user, "can_read", "document", tuples, page)
ids, cursor, err := checker.ListSubjectsWithContextualTuples(ctx, user, "can_read", "user", tuples, page)If tuples is empty, these methods delegate to their non-contextual equivalents.
SQL-Level Implementation
Contextual tuples work by temporarily shadowing the melange_tuples view:
- A session-scoped temporary table
melange_contextual_tuplesis created. - The contextual tuples are inserted into this table.
- A temporary view
melange_tuplesis created thatUNION ALLs the base view with the temporary table. - The permission check runs against this combined view.
- Both temporary objects are dropped after the check.
Because PostgreSQL resolves temporary objects before schema-qualified ones, the generated SQL functions see the combined tuples without modification.
Connection Requirements
Temporary objects in PostgreSQL are session-scoped (tied to a specific connection). The setup, check, and cleanup must all happen on the same connection.
| Querier Type | Contextual Tuples |
|---|---|
*sql.DB | Supported. A dedicated connection is acquired from the pool, pinned for the operation, and returned afterward. |
*sql.Tx | Supported. Already on a single connection. Also sees uncommitted changes. |
*sql.Conn | Supported. Already pinned to a single connection. |
Custom Querier | Returns ErrContextualTuplesUnsupported. |
Bulk Check with Contextual Tuples
results, err := checker.NewBulkCheck(ctx).
Add(user, "can_read", doc1).
Add(user, "can_write", doc1).
WithContextualTuples(
melange.ContextualTuple{
Subject: melange.Object{Type: "user", ID: "alice"},
Relation: melange.Relation("ip_allowed"),
Object: melange.Object{Type: "network", ID: "office"},
},
).
Execute()The contextual tuples are set up once before all checks in the batch and cleaned up afterward.
Validation
Contextual tuples are validated before setup. With a Validator attached to the checker, each tuple is checked against the schema. Without a validator, basic shape validation runs:
- If a subject ID contains
#(userset format likegroup:123#member), the part after#must not be empty.
Invalid tuples return ErrInvalidContextualTuple with the tuple index.
Caching
Checks with contextual tuples bypass the cache entirely. Each call goes directly to the database. This is by design: contextual tuples are unique to each request, so cached results would be incorrect.
Performance
Contextual tuple checks have additional overhead compared to regular checks:
- ~3-5ms setup per operation (creating temporary table, inserting tuples, creating temporary view).
- Regular check latency on top of setup.
- Cleanup after each operation.
For high-throughput paths, consider whether the authorization data can be modelled in your domain tables instead.
Example Patterns
IP Allowlist
Schema:
type network
relations
define ip_allowed: [user]
type resource
relations
define can_access: [user] and ip_allowed from networkApplication code:
tuples := []melange.ContextualTuple{
{
Subject: melange.Object{Type: "user", ID: userID},
Relation: melange.Relation("ip_allowed"),
Object: melange.Object{Type: "network", ID: "corporate"},
},
}
// Only inject the tuple if the IP is in the allowlist
if isAllowedIP(clientIP) {
allowed, err = checker.CheckWithContextualTuples(ctx, user, "can_access", resource, tuples)
} else {
allowed, err = checker.Check(ctx, user, "can_access", resource)
}Temporal Access
// Grant access only during a maintenance window
var tuples []melange.ContextualTuple
if time.Now().Before(maintenanceEnd) {
tuples = append(tuples, melange.ContextualTuple{
Subject: melange.Object{Type: "user", ID: userID},
Relation: melange.Relation("maintenance_access"),
Object: melange.Object{Type: "system", ID: "prod"},
})
}
allowed, err := checker.CheckWithContextualTuples(ctx, user, "can_deploy", system, tuples)Next Steps
- Checking Permissions: core Checker API
- Go API: full method signatures and types
- Errors:
ErrContextualTuplesUnsupportedandErrInvalidContextualTuple