Aphex

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:

src/routes/api/my-endpoint/+server.ts
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; // boolean

findByID

Find a single document by ID.

const post = await api.collections.post.findByID(context, 'doc_123', {
	perspective: 'published'
});

// Returns the document or null

count

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 found

Publish 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 boolean

publish

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 null

unpublish

Revert a document to draft-only state.

const draft = await api.collections.post.unpublish(context, 'doc_123');
// Returns the draft document or null

Singletons

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:

OptionTypeDefaultDescription
whereWhere-Filter conditions.
limitnumber50Max results per page.
offsetnumber0Number of results to skip.
sortstring | string[]-Sort order. Prefix with - for descending (e.g. '-publishedAt').
depthnumber0Reference resolution depth.
selectstring[]-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:

OperationRequired capability
find / findByID / countdocument.read
createdocument.create
updatedocument.update
deletedocument.delete
publishdocument.publish
unpublishdocument.unpublish

Built-in role mapping for reference:

Rolereadcreate/update/deletepublish / unpublish
vieweryesnono
editoryesyesyes
adminyesyesyes
owneryesyesyes

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

src/routes/api/posts/+server.ts
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

src/routes/blog/[slug]/+page.server.ts
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

scripts/seed.ts
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 }
);
Edit on GitHub

Last updated on