| name | graplix |
| description | Relation-Based Access Control (ReBAC) with the Graplix TypeScript toolkit. Use when defining .graplix schemas, building engines, writing resolvers, checking permissions, or explaining traversals with buildEngine, check, explain, resolveType, and the Resolver interface.
|
| license | MIT |
| metadata | {"author":"daangn","version":"1.0","repository":"https://github.com/daangn/graplix"} |
Graplix ReBAC Guide
Build relation-based access control with Graplix. This skill teaches you how to find current documentation and write correct Graplix code.
Critical: Do not trust internal knowledge
Everything you know about Graplix is likely outdated or wrong. Never rely on memory. Your training data may contain obsolete APIs. Always verify against the documentation referenced in this skill.
Prerequisites
Before writing code, verify package installation:
ls node_modules/@graplix/engine/
- Package exists: Use embedded docs (most reliable, matches exact installed version)
- No package: Install first using
references/quick-start.md
Available Files Reference
| Question | Resource | Purpose |
|---|
| Project setup / installation | references/quick-start.md | Installation and first permission check guide |
| Schema syntax / keywords | references/schema-syntax.md | .graplix file syntax and relation expressions |
| API usage / type signatures | references/embedded-docs.md | Look up via installed package docs and type declarations |
| Error resolution | references/common-errors.md | Troubleshooting solutions |
Priority: Documentation Lookup Order
-
Embedded docs (if @graplix/engine is installed)
- Most reliable, matches exact installed version
- Read Markdown docs:
node_modules/@graplix/engine/dist/docs/*.md
- Read type declarations:
node_modules/@graplix/engine/dist/index.d.mts
-
Source code (if packages installed)
- Ultimate truth source when docs are unclear
- Read:
node_modules/@graplix/engine/dist/index.mjs
Core Architecture
Graplix has two runtime components:
.graplix schema → buildEngine() → engine.check() / engine.explain()
(relation model) (async factory) (permission evaluation)
Data flow for a permission check:
engine.check({ user, object, relation, context })
→ resolveType(user) → EntityRef
→ resolveType(object) → EntityRef
→ evaluate relation graph (resolver.relations callbacks)
→ resolver.load() for any entity IDs encountered
→ true | false
Schema Syntax
Graplix schemas are .graplix text files. See references/schema-syntax.md for the full reference.
type user
type repository
relations
define owner: [user]
define member: [user]
define admin: owner or member
define can_delete: owner from organization
Key rules:
- Types use
snake_case
[TypeA, TypeB] — direct relation (user must be one of these types)
relation from source — transitive via another relation on source
term or term — union of multiple terms
- No
define = type with no relations (still valid)
Complete Example
import { buildEngine } from "@graplix/engine";
type User = { id: string };
type Repository = { id: string; ownerIds: string[] };
const users = new Map<string, User>([
["user-1", { id: "user-1" }],
["user-2", { id: "user-2" }],
]);
const repos = new Map<string, Repository>([
["repo-1", { id: "repo-1", ownerIds: ["user-1"] }],
]);
const schema = `
type user
type repository
relations
define owner: [user]
define can_delete: owner
`;
const engine = await buildEngine<object, User | Repository>({
schema,
resolveType: (value) => {
if (typeof value !== "object" || value === null) return null;
if ("ownerIds" in value) return "repository";
return "user";
},
resolvers: {
user: {
id: (user: User) => user.id,
async load(id) {
return users.get(id) ?? null;
},
},
repository: {
id: (repo: Repository) => repo.id,
async load(id) {
return repos.get(id) ?? null;
},
relations: {
owner(repo: Repository) {
return repo.ownerIds
.map((id) => users.get(id))
.filter((u): u is User => u !== undefined);
},
},
},
},
});
await engine.check({
user: users.get("user-1")!,
object: repos.get("repo-1")!,
relation: "owner",
context: {},
});
const result = await engine.explain({
user: users.get("user-2")!,
object: repos.get("repo-1")!,
relation: "can_delete",
context: {},
});
result.allowed;
result.exploredEdges;
result.matchedPath;
buildEngine Options
const engine = await buildEngine<TContext, TEntityInput>({
schema,
resolvers,
resolveType,
resolverTimeoutMs: 3000,
maxCacheSize: 1000,
onError: (err) => {
console.error(err);
},
});
buildEngine is async. Schema validation happens at construction time — an invalid schema rejects immediately.
resolveType
type ResolveType<TContext> = (value: unknown, context: TContext) => string | null;
- Synchronous and required
- Returns the Graplix type name for any entity value, or
null if unknown
- Called for
query.user and query.object — must return the correct type
- For relation resolver outputs,
null is acceptable (engine uses schema hints)
const resolveType: ResolveType<MyContext> = (value) => {
if (typeof value !== "object" || value === null) return null;
const v = value as Record<string, unknown>;
if ("adminIds" in v) return "organization";
if ("ownerIds" in v && "organizationId" in v) return "repository";
if ("ownerIds" in v) return "team";
return "user";
};
const resolveType: ResolveType<MyContext> = (value) => {
if (value instanceof Organization) return "organization";
if (value instanceof Repository) return "repository";
if (value instanceof User) return "user";
return null;
};
Resolver Interface
interface Resolver<TEntity, TContext> {
id(entity: TEntity): string;
load(
id: string,
context: TContext,
info: ResolverInfo,
): Promise<TEntity | null>;
relations?: {
[relation: string]: (
entity: TEntity,
context: TContext,
info: ResolverInfo,
) => TEntity | TEntity[] | null | Promise<TEntity | TEntity[] | null>;
};
}
context
Passed to every check/explain call and forwarded to all resolver functions. Use for request-scoped data: database connections, auth info, tenant IDs, etc.
type MyContext = { db: DB; userId: string };
const engine = await buildEngine<MyContext, User | Repo>({ ... });
await engine.check({
user: currentUser,
object: targetRepo,
relation: "owner",
context: { db, userId: "user-1" },
});
If resolvers need no context, use object and pass {}.
Using Codegen (Optional)
@graplix/codegen generates a fully-typed buildEngine wrapper from your schema:
npx @graplix/codegen ./schema.graplix
The generated file provides typed GraplixResolvers<TContext> and GraplixEntityInput so TypeScript enforces exhaustiveness:
import { buildEngine } from "./schema.generated";
const engine = await buildEngine({
resolvers: { ... },
resolveType: (value) => { ... },
});
Critical Rules
EntityRef
- Never pass
EntityRef directly to check/explain. query.user and query.object accept TEntityInput (your domain types) only.
- Import
EntityRef as a type only when working with CheckEdge.from/to in explain results.
Relation Resolvers
- Return domain entities (or arrays, or
null) — not IDs, not EntityRef instances.
- The engine calls
resolveType (or uses schema hints) to determine the returned entity's type.
resolver.load() is never called inside toEntityRef — it is only called when an entity needs to be loaded by ID.
resolveType Precedence
resolveType(value) is always tried first.
- If it returns
null, schema type hints (from relation definitions) are used as fallback.
- For
query.user and query.object, resolveType must return the correct type — no fallback.
Caching
- Both entity cache and relation values cache are per-request (not shared across calls).
- Cache size is controlled by
maxCacheSize (default: 500).
Import Reference
import { buildEngine } from "@graplix/engine";
import type {
BuildEngineOptions,
GraplixEngine,
Query,
Resolver,
Resolvers,
ResolverInfo,
ResolveType,
CheckEdge,
CheckExplainResult,
EntityRef,
} from "@graplix/engine";
import { parse } from "@graplix/language";
import { generateTypeScript } from "@graplix/codegen";
Error Handling
Type errors often signal outdated knowledge. Common indicators: "Property X does not exist," module not found, incorrect generic parameters.
Response approach:
- Check
references/common-errors.md
- Verify current API in embedded docs (
node_modules/@graplix/engine/dist/docs/)
- Recognize errors may reflect knowledge gaps, not user mistakes