On this page
  1. Declare the role
  2. Mount the screen
  3. Add the person
  4. Verify it
  5. Related reference

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.

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.

Edit this page on GitHub(opens in a new tab)