organizationContext
organizationContext marks a field in your function inputs as injected from the auth result’s organization identity. These fields are hidden from API callers but available in resolvers for multi-tenant applications.
Signature
function organizationContext<T extends z.ZodType>(schema: T): TParameters
schema
Type: z.ZodType
Required: Yes
A Zod schema defining the shape of the organization context data.
Usage
1. Return organization from auth
Your auth function must return organization identity:
defineOntology({ auth: async (req) => { const url = new URL(req.url); const orgId = url.searchParams.get('org_id');
if (!orgId) { return { groups: ['public'] }; }
const user = await validateToken(req); const org = await db.organizations.findById(orgId);
// Verify user membership in organization const isMember = await db.organizationMembers.exists({ userId: user.id, orgId });
if (!isMember) { throw new Error('Not a member of this organization'); }
return { groups: ['member'], organization: { id: org.id, name: org.name, plan: org.plan }, }; }, // ...});2. Declare organizationContext in inputs
Mark fields that should be injected:
import { organizationContext, z } from 'ont-run';import createProject from './resolvers/createProject.js';
functions: { createProject: { description: 'Create a project in the organization', access: ['member'], entities: ['Project'], inputs: z.object({ name: z.string(), description: z.string().optional(), // This field is injected, not provided by caller currentOrg: organizationContext(z.object({ id: z.string(), name: z.string(), plan: z.string(), })), }), resolver: createProject, },}3. Use in resolver
The resolver receives the typed organization data:
export default async function createProject( ctx: ResolverContext, args: { name: string; description?: string; currentOrg: { id: string; name: string; plan: string }; }) { // args.currentOrg is automatically populated return db.projects.create({ name: args.name, description: args.description, organizationId: args.currentOrg.id, });}Behavior
Hidden from callers
Fields marked with organizationContext() are stripped from:
- REST API schemas
- MCP tool definitions
- OpenAPI documentation
Callers never see or provide these fields.
Injected at runtime
Before validation, the framework merges auth().organization into the request args:
// Caller sends:{ "name": "My Project", "description": "A new project" }
// Framework injects organization:{ "name": "My Project", "description": "A new project", "currentOrg": { "id": "org_123", "name": "Acme Corp", "plan": "enterprise" }}
// Resolver receives the merged objectStartup validation
At server startup, ont-run validates that functions using organizationContext() will receive organization data. If your auth function doesn’t return an organization field, you’ll get an error:
Error: The following functions use organizationContext() but auth() does not return an organization object: createProject, listProjects
To fix this, update your auth function to return an AuthResult: auth: async (req) => { const orgId = new URL(req.url).searchParams.get('org_id'); return { groups: ['member'], organization: { id: orgId, name: '...' } // Add organization data here }; }Review UI
The review UI shows an “Org Context” badge on functions that use organizationContext(), making it visible during security review.
Example: Multi-tenant data isolation
A common pattern for multi-tenant applications is using organization context to ensure data isolation:
import listProjects from './resolvers/listProjects.js';import updateProject from './resolvers/updateProject.js';
functions: { listProjects: { description: 'List projects in the organization', access: ['member'], entities: ['Project'], inputs: z.object({ currentOrg: organizationContext(z.object({ id: z.string() })), }), resolver: listProjects, },
updateProject: { description: 'Update a project', access: ['member'], entities: ['Project'], inputs: z.object({ projectId: z.string(), name: z.string(), currentOrg: organizationContext(z.object({ id: z.string() })), }), resolver: updateProject, },}export default async function listProjects( ctx: ResolverContext, args: { currentOrg: { id: string } }) { // Automatically filtered to organization return db.projects.findMany({ where: { organizationId: args.currentOrg.id } });}export default async function updateProject( ctx: ResolverContext, args: { projectId: string; name: string; currentOrg: { id: string } }) { const project = await db.projects.findById(args.projectId);
// Ensure project belongs to the organization if (project.organizationId !== args.currentOrg.id) { throw new Error('Project not found in this organization'); }
return db.projects.update(args.projectId, { name: args.name });}Combining with userContext
For applications that need both user and organization context, you can use both:
inputs: z.object({ projectId: z.string(), currentUser: userContext(z.object({ id: z.string() })), currentOrg: organizationContext(z.object({ id: z.string() })),})The resolver will receive both injected contexts:
export default async function resolver( ctx: ResolverContext, args: { projectId: string; currentUser: { id: string }; currentOrg: { id: string }; }) { // Both currentUser and currentOrg are available}Typical auth patterns
Query parameter-based
auth: async (req) => { const url = new URL(req.url); const orgId = url.searchParams.get('org_id');
if (!orgId) return { groups: ['public'] };
const user = await verifyToken(req); await verifyOrgMembership(user.id, orgId);
const org = await db.organizations.findById(orgId); return { groups: ['member'], organization: { id: org.id, name: org.name }, };}Header-based
auth: async (req) => { const orgId = req.headers.get('X-Organization-ID');
if (!orgId) return { groups: ['public'] };
const user = await verifyToken(req); await verifyOrgMembership(user.id, orgId);
const org = await db.organizations.findById(orgId); return { groups: ['member'], organization: { id: org.id, name: org.name }, };}Subdomain-based
auth: async (req) => { const url = new URL(req.url); const subdomain = url.hostname.split('.')[0];
const org = await db.organizations.findBySubdomain(subdomain); if (!org) return { groups: ['public'] };
const user = await verifyToken(req); await verifyOrgMembership(user.id, org.id);
return { groups: ['member'], organization: { id: org.id, name: org.name }, };}