SvelteKit RuneLayer is a CMS-as-a-package for SvelteKit apps. It runs inside your existing Node process with schema-driven content, auth, query APIs, hooks, and an admin UI.
RuneLayer is shipping as 0.x (alpha/beta maturity). The APIs are usable and tested, but you should expect iterative changes while the platform hardens toward a v1 line.
handle, admin loaders/actions, request-scoped query helpers>= 22.18.0 (repo pinned via .nvmrc)10+2.x5.xpnpm add @flaming-codes/sveltekit-runelayer
pnpm add -D drizzle-kit
// src/lib/server/schema.ts
import { defineCollection, text, slug, richText } from "@flaming-codes/sveltekit-runelayer";
export const Posts = defineCollection({
slug: "posts",
fields: [
{ name: "title", ...text({ required: true }) },
{ name: "slug", ...slug({ from: "title" }) },
{ name: "content", ...richText() },
],
});
drizzle-kit discovers Drizzle table instances from top-level named exports only. Destructure and re-export each table individually:
// src/lib/server/drizzle-schema.ts
import { createDrizzleKitSchema } from "@flaming-codes/sveltekit-runelayer/drizzle";
import { Posts } from "./schema.js";
const _schema = createDrizzleKitSchema([Posts]);
export const { posts, user, session, account, verification } = _schema;
If your app uses globals, pass them as the second argument so drizzle-kit also exports
__runelayer_globals (and __runelayer_global_versions when global versioning is enabled).
// drizzle.config.ts
import { defineConfig } from "drizzle-kit";
import { defineRunelayerDrizzleConfig } from "@flaming-codes/sveltekit-runelayer/sveltekit/drizzle";
export default defineConfig(
defineRunelayerDrizzleConfig({
schema: "./src/lib/server/drizzle-schema.ts",
out: "./drizzle",
database: {
url: process.env.DATABASE_URL ?? "file:./data/sveltekit-runelayer.db",
authToken: process.env.DATABASE_AUTH_TOKEN,
},
}),
);
// src/lib/server/runelayer.ts
import { redirect, error, fail } from "@sveltejs/kit";
import { createRunelayerApp } from "@flaming-codes/sveltekit-runelayer/sveltekit/server";
import { Posts } from "./schema.js";
let _runelayer: ReturnType<typeof createRunelayerApp> | undefined;
export function getRunelayerApp() {
if (!_runelayer) {
_runelayer = createRunelayerApp({
kit: { redirect, error, fail },
collections: [Posts],
auth: {
secret: process.env.AUTH_SECRET ?? "dev-secret-change-in-production",
baseURL: process.env.ORIGIN ?? "http://localhost:5173",
},
database: {
url: process.env.DATABASE_URL ?? "file:./data/sveltekit-runelayer.db",
authToken: process.env.DATABASE_AUTH_TOKEN,
},
admin: {
path: "/admin",
},
});
}
return _runelayer;
}
// src/hooks.server.ts
import { createRunelayerHandle } from "@flaming-codes/sveltekit-runelayer/sveltekit/server";
import { getRunelayerApp } from "$lib/server/runelayer";
export const handle = createRunelayerHandle(getRunelayerApp);
// src/routes/(admin)/admin/[...path]/+page.server.ts
import { createRunelayerAdminRoute } from "@flaming-codes/sveltekit-runelayer/sveltekit/server";
import { getRunelayerApp } from "$lib/server/runelayer";
export const { load, actions } = createRunelayerAdminRoute(getRunelayerApp);
<!-- src/routes/(admin)/admin/[...path]/+page.svelte -->
<script lang="ts">
import { AdminRoutePage } from "@flaming-codes/sveltekit-runelayer/sveltekit/components";
import type { RunelayerAdminPageProps } from "@flaming-codes/sveltekit-runelayer/sveltekit/components";
let { data, form }: RunelayerAdminPageProps = $props();
</script>
<AdminRoutePage {data} {form} />
npx drizzle-kit generate
npx drizzle-kit migrate
// src/routes/(site)/+page.server.ts
import { getRunelayerApp } from "$lib/server/runelayer";
import { Posts } from "$lib/server/schema";
export async function load({ request }) {
const runelayer = getRunelayerApp();
const posts = await runelayer.withRequest(request).find(Posts, {
limit: 10,
sort: "createdAt",
sortOrder: "desc",
});
return { posts };
}
redirect, error, and fail into kit during createRunelayerApp.createRunelayerHandle(getRunelayerApp) in hooks.server.ts.createRunelayerAdminRoute(getRunelayerApp) in your admin +page.server.ts.runelayer.withRequest(request) for request-bound access checks.runelayer.system only for trusted server-side tasks (seeding, internal jobs, migrations)./admin.Add this alias in your app vite.config.ts so production builds resolve Better Auth against zod v4:
resolve: {
alias: {
zod: "zod/v4",
},
}