Local API
Query and mutate your content directly from the server using the type-safe Local API.
The Local API is the core data layer of Aphex. Both the REST API and GraphQL API are thin wrappers around it. You can use it directly in SvelteKit route handlers, server load functions, and scripts for full control over your content.
Accessing the Local API
The Local API is available on event.locals.aphexCMS.localAPI in any SvelteKit server context:
import { json } from '@sveltejs/kit';
import { authToContext } from '@aphexcms/cms-core/server';
export const GET = async ({ locals }) => {
const api = locals.aphexCMS.localAPI;
const context = authToContext(locals.auth);
const result = await api.collections.post.find(context, {
where: { status: { equals: 'published' } },
limit: 10
});
return json({ data: result.docs });
};Context
Every Local API operation requires a LocalAPIContext. This tells the API who is making the request and which organization the data belongs to.
From an authenticated request
Use authToContext() to convert locals.auth into a context. This works with both session auth and API key auth:
import { authToContext } from '@aphexcms/cms-core/server';
const context = authToContext(locals.auth);System context (bypass permissions)
For seed scripts, cron jobs, or migrations where there is no user session, use systemContext(). This sets overrideAccess: true, bypassing all permission checks and row-level security:
import { systemContext } from '@aphexcms/cms-core/server';
const context = systemContext('your-organization-id');Context shape
interface LocalAPIContext {
organizationId: string; // Required for multi-tenancy
user?: CMSUser; // For permission checks and audit trails
overrideAccess?: boolean; // Bypass RLS and permissions (default: false)
auth?: Auth; // Full auth object for custom logic
}Collections
Each document type in your schema becomes a collection on localAPI.collections. Collections provide CRUD methods:
const api = locals.aphexCMS.localAPI;
api.collections.post; // CollectionAPI for the 'post' document type
api.collections.page; // CollectionAPI for the 'page' document type
api.collections.product; // etc.You can also check what collections exist:
api.getCollectionNames(); // ['post', 'page', 'product']
api.hasCollection('post'); // true
api.getCollectionSchema('post'); // SchemaType for 'post'Type-safe collections
Run pnpm generate:types to generate TypeScript interfaces from your schemas. This creates module augmentation that makes localAPI.collections fully typed — you get autocompletion for collection names and return types. See Type Generation for details.
Methods
find
Find multiple documents with filtering, sorting, and pagination.
const result = await api.collections.post.find(context, {
where: { status: { equals: 'published' } },
sort: '-publishedAt',
limit: 20,
offset: 0,
perspective: 'published'
});
result.docs; // Post[]
result.totalDocs; // Total matching documents
result.totalPages; // Total pages
result.hasNextPage; // boolean
result.hasPrevPage; // booleanfindByID
Find a single document by ID.
const post = await api.collections.post.findByID(context, 'doc_123', {
perspective: 'published'
});
// Returns the document or nullcount
Count documents matching a filter.
const total = await api.collections.post.count(context, {
where: { status: { equals: 'published' } }
});create
Create a new document. Returns the document and validation results.
const result = await api.collections.post.create(context, {
title: 'My New Post',
slug: 'my-new-post',
body: 'Hello world.'
});
result.document; // The created document (draft)
result.validation; // { isValid: boolean, errors: [...] }Pass { publish: true } to publish immediately. This will throw if validation fails:
const result = await api.collections.post.create(
context,
{ title: 'Published Post', slug: 'published-post' },
{ publish: true }
);update
Update an existing document. Merges the provided data with existing fields.
const result = await api.collections.post.update(context, 'doc_123', { title: 'Updated Title' });
// Returns DocumentResult or null if not foundPublish after updating:
const result = await api.collections.post.update(
context,
'doc_123',
{ title: 'Updated Title' },
{ publish: true }
);delete
Delete a document by ID.
const deleted = await api.collections.post.delete(context, 'doc_123');
// Returns booleanpublish
Publish a document. Validates the draft data first and throws if validation fails.
const published = await api.collections.post.publish(context, 'doc_123');
// Returns the published document or nullunpublish
Revert a document to draft-only state.
const draft = await api.collections.post.unpublish(context, 'doc_123');
// Returns the draft document or nullSingletons
Schemas marked singleton: true expose a different surface — there is at most one row, so most of the collection methods don't apply. The codegen narrows the autocompletion to a SingletonCollection<T> so invalid calls fail at compile time.
// Lazy-creates the canonical row on first access
const nav = await api.collections.siteNavigation.get(context, {
perspective: 'published'
});
// Update by name — no id needed
await api.collections.siteNavigation.update(context, nav.id, {
brand: 'Aphex'
});
// Compute the deterministic id (rarely needed)
const id = api.collections.siteNavigation.getSingletonId(context);find() still works on a singleton (it returns a one-element page) so generic helpers can keep treating every schema the same way. create() and delete() throw SingletonOperationError. See Singletons for the full guide.
Version history
Each update (and every publish) writes a snapshot to cms_document_versions. Use localAPI.versionService directly for scripts and migrations:
const adapter = locals.aphexCMS.databaseAdapter;
const ctx = authToContext(locals.auth);
const { versions, total } = await api.versionService.listVersions(
adapter,
ctx.organizationId,
'doc_xyz',
{ limit: 25, offset: 0 }
);
const restored = await api.versionService.restoreVersion(
adapter,
ctx.organizationId,
'doc_xyz',
8,
ctx.user?.id
);Restoring replaces the draft with the snapshot's data and writes a new draft-event version recording the action. See Version History for the HTTP endpoints and admin UI.
Filtering
The where option accepts a database-agnostic filter object.
Comparison operators
where: { title: { equals: 'Hello' } }
where: { title: { not_equals: 'Hello' } }
where: { status: { in: ['draft', 'published'] } }
where: { status: { not_in: ['archived'] } }
where: { image: { exists: true } }Numeric and date comparisons
where: {
price: {
greater_than: 10;
}
}
where: {
price: {
greater_than_equal: 10;
}
}
where: {
price: {
less_than: 100;
}
}
where: {
price: {
less_than_equal: 100;
}
}String operations
where: {
title: {
contains: 'blog';
}
}
where: {
title: {
starts_with: 'How';
}
}
where: {
title: {
ends_with: '?';
}
}
where: {
title: {
like: '%blog%';
}
}Logical operators
Multiple conditions at the top level are combined with AND:
where: {
status: { equals: 'published' },
title: { contains: 'blog' }
}Use or for OR logic:
where: {
or: [{ title: { contains: 'tutorial' } }, { title: { contains: 'guide' } }];
}Use and for explicit AND grouping:
where: {
and: [{ title: { contains: 'blog' } }, { body: { exists: true } }];
}Nested field filters
Use dot notation for nested fields:
where: { 'seo.metaTitle': { contains: 'blog' } }
where: { 'author.name': { equals: 'John' } }Find Options
The full set of options for find:
| Option | Type | Default | Description |
|---|---|---|---|
where | Where | - | Filter conditions. |
limit | number | 50 | Max results per page. |
offset | number | 0 | Number of results to skip. |
sort | string | string[] | - | Sort order. Prefix with - for descending (e.g. '-publishedAt'). |
depth | number | 0 | Reference resolution depth. |
select | string[] | - | Only return specified fields. |
perspective | 'draft' | 'published' | 'draft' | Which version of the document to return. |
Perspectives
Documents in Aphex have two versions: draft and published.
'draft'(default) - Returns the working copy with unpublished changes.'published'- Returns the last published version. Documents that have never been published won't appear.
// Get published content for the public site
const published = await api.collections.post.find(context, {
perspective: 'published'
});
// Get draft content for the admin UI
const drafts = await api.collections.post.find(context, {
perspective: 'draft'
});Document Shape
Documents returned by the Local API include your content fields plus a _meta object:
{
id: 'doc_123',
title: 'My Post', // Your fields
slug: 'my-post',
body: '...',
_meta: {
type: 'post',
status: 'draft',
organizationId: 'org_123',
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T15:45:00Z',
createdBy: 'user_123',
updatedBy: 'user_123',
publishedAt: null,
publishedHash: null
}
}Permissions
The Local API enforces the same capability-based access control as the HTTP and GraphQL APIs. Each operation is gated by a specific capability:
| Operation | Required capability |
|---|---|
find / findByID / count | document.read |
create | document.create |
update | document.update |
delete | document.delete |
publish | document.publish |
unpublish | document.unpublish |
Built-in role mapping for reference:
| Role | read | create/update/delete | publish / unpublish |
|---|---|---|---|
viewer | yes | no | no |
editor | yes | yes | yes |
admin | yes | yes | yes |
owner | yes | yes | yes |
Custom roles grant whatever capabilities you assign them. Schema-level access rules and field-level access rules apply on top of this check — see Access Control.
Set overrideAccess: true in the context to bypass all checks (for system operations only).
Examples
Public API endpoint
import { json } from '@sveltejs/kit';
import { authToContext } from '@aphexcms/cms-core/server';
export const GET = async ({ locals, url }) => {
const api = locals.aphexCMS.localAPI;
const context = authToContext(locals.auth);
const page = parseInt(url.searchParams.get('page') || '1');
const limit = 10;
const result = await api.collections.post.find(context, {
where: { status: { equals: 'published' } },
perspective: 'published',
sort: '-publishedAt',
limit,
offset: (page - 1) * limit
});
return json({
posts: result.docs,
totalPages: result.totalPages,
hasNextPage: result.hasNextPage
});
};Server load function
import { error } from '@sveltejs/kit';
import { authToContext } from '@aphexcms/cms-core/server';
export const load = async ({ locals, params }) => {
const api = locals.aphexCMS.localAPI;
const context = authToContext(locals.auth);
const result = await api.collections.post.find(context, {
where: { slug: { equals: params.slug } },
perspective: 'published',
limit: 1
});
const post = result.docs[0];
if (!post) throw error(404, 'Post not found');
return { post };
};Seed script
import { getLocalAPI, systemContext } from '@aphexcms/cms-core/server';
const api = getLocalAPI();
const context = systemContext('your-org-id');
await api.collections.post.create(
context,
{
title: 'Welcome to Aphex',
slug: 'welcome',
body: 'Your first post.'
},
{ publish: true }
);Last updated on