Give a role its own admin area
A role doesn’t have to reach cairn’s content at all to live under /admin. This guide builds an
instructor role: it signs in with the same magic link as every other editor, sees none of
cairn’s own content surfaces (no post list, no Library, no Settings), and lands straight on a
class roster screen the site built and owns. The pattern is the none capability from the
declared role vocabulary: an authenticated identity cairn’s own
surfaces refuse, that a site’s own custom route is free to admit. If you haven’t declared your
adapter yet, start with Define an adapter and schema.
Declare the role
Add instructor to your adapter’s role vocabulary with defineRoles, mapped to none capability
and a home, the /admin route it lands on after sign-in:
// src/lib/cairn.config.ts
import { defineAdapter, defineRoles } from '@glw907/cairn-cms';
export const roles = defineRoles({
owner: 'owner',
instructor: { capability: 'none', home: '/admin/classes' },
});
export const cairn = defineAdapter({
// ...content, backend, email, rendering...
roles,
});
instructor is a name outside the engine’s default owner/editor pair, so the editor.role
column’s CHECK constraint rejects it until you apply the 0001_roles.sql migration the engine
ships in the package. See the migration section of Configure auth and
D1 for copying it out of
node_modules/@glw907/cairn-cms/migrations/ and the wrangler d1 migrations apply step; a site
already on a larger vocabulary has this applied already.
Mount the screen
A /admin/classes route is an ordinary SvelteKit route dropped next to cairn’s own admin
routes, the same seam Add a custom admin screen covers in full.
The auth guard already ran and set a populated, typed event.locals.cairnEditor before this route’s
load does, for an instructor session exactly as for an owner or an editor: a none-capability
session still authenticates and reaches this route untouched. Nothing about none blocks the
route from resolving, so gate it yourself.
The recommended path is the access map: declare which role reaches
this path and let requireAccess check it, the same one-authority-function canReach the
sidebar itself reads, so a change to who reaches /admin/classes never has to be made twice.
// src/lib/cairn.access.ts
import { defineAccess } from '@glw907/cairn-cms';
import { roles } from './cairn.config.js';
export const access = defineAccess(roles, {
'/admin/classes': ['instructor'],
});
// src/routes/admin/classes/+page.server.ts
import { requireAccess } from '@glw907/cairn-cms/sveltekit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = (event) => {
const editor = requireAccess(event); // denies every role but instructor (and owner)
return { displayName: editor.displayName };
};
A bare role or capability check on requireSession still works when a map is more than the
screen needs, or when the check depends on something the map can’t express:
// src/routes/admin/classes/+page.server.ts (a hand-rolled check, the alternative to the map)
import { error } from '@sveltejs/kit';
import { requireSession } from '@glw907/cairn-cms/sveltekit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = (event) => {
const editor = requireSession(event);
if (editor.role !== 'instructor' && editor.capability !== 'owner') {
error(403, 'This screen is for instructors.');
}
return { displayName: editor.displayName };
};
requireEditor is the other common shape: call it
instead of requireSession when a screen should refuse every none-capability role rather than
admit one by name.
The shared /admin/+layout.svelte still wraps this route in
CairnAdminShell, the same chrome every built-in
view renders inside, so the instructor sees the site’s own top bar and sidebar, not a bare page.
For a none-capability session the shell drops every built-in engine entry (Library, Tags, the
nav-menu editor, Settings, Help), since each one would refuse the request with a 403 anyway;
only the site’s own custom nav renders. Because instructor declares a home, signing in lands
the instructor on /admin/classes directly; a none-capability role with no declared home
lands on a calm signed-in welcome view instead.
Add the person
ManageEditors, the owner-only screen at
/admin/editors, renders every declared role in its role control once your vocabulary holds more
than the default pair: an owner picks instructor from the list the same way they pick editor
today. For the very first owner on a brand-new site, before any row exists to grant one,
Configure auth and D1 covers bootstrapOwner
and the direct D1 seed, either of which gets that first row in.
Verify it
Sign in as the instructor and confirm the session lands on /admin/classes with none of cairn’s
own content surfaces in the sidebar. Try /admin (or any built-in view’s URL) directly as that
same session and confirm it answers 403, not a redirect to sign in again: a none-capability
session is still authenticated, just refused by cairn’s own surfaces. Sign in as an owner and
confirm /admin/classes still opens, since the preceding load admits owner capability too.
Related reference
The declared role vocabulary documents defineRoles and the
capability-resolution helpers this guide used.
Restrict admin access by role covers defineAccess,
requireAccess, and the deny-not-hide doctrine this guide’s recommended path builds on.
requireSession and
requireEditor document the none contract in full, and
Add a custom admin screen covers the custom-route seam and
adminAction in depth. ManageEditors and
CairnAdminShell document the roster screen and the
shell this guide’s screen rendered inside.