On this page
  1. Declare the role
  2. Narrow the Role type
  3. Mount the screen
  4. Add the person
  5. Verify it
  6. 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.

Narrow the Role type

Augment CairnRolesRegister once in your app.d.ts so locals.editor.role narrows to your declared names everywhere it’s read, custom routes included, instead of staying the unaugmented 'owner' | 'editor':

// src/app.d.ts
import { roles } from './lib/cairn.config.js';

declare module '@glw907/cairn-cms' {
  interface CairnRolesRegister {
    roles: typeof roles;
  }
}

The CairnRolesRegister reference covers what this augmentation changes and what it doesn’t: it’s a read-side type only, and it has no effect on which capability a role resolves to at runtime. It’s also the type a navLayout node’s roles list narrows against once you declare one: see Organize your admin nav for a role-gated section built on the same vocabulary this guide declares.

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.editor 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, on the role or the capability, whichever your screen means to check:

// src/routes/admin/classes/+page.server.ts
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, CairnRolesRegister, and the capability-resolution helpers this guide used. 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)