Index: tabase/drizzle/queries/todos.ts
===================================================================
--- database/drizzle/queries/todos.ts	(revision 66bf4fdd2573ef9770d4f2d8dc1e5a8f3f842a7c)
+++ 	(revision )
@@ -1,10 +1,0 @@
-import type { dbSqlite } from "../db";
-import { todoTable } from "../schema/todos";
-
-export function insertTodo(db: ReturnType<typeof dbSqlite>, text: string) {
-  return db.insert(todoTable).values({ text });
-}
-
-export function getAllTodos(db: ReturnType<typeof dbSqlite>) {
-  return db.select().from(todoTable).all();
-}
Index: tabase/drizzle/schema/todos.ts
===================================================================
--- database/drizzle/schema/todos.ts	(revision 66bf4fdd2573ef9770d4f2d8dc1e5a8f3f842a7c)
+++ 	(revision )
@@ -1,11 +1,0 @@
-import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
-
-// Example of defining a schema in Drizzle ORM:
-export const todoTable = sqliteTable("todos", {
-  id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }),
-  text: text("text", { length: 50 }).notNull(),
-});
-
-// You can then infer the types for selecting and inserting
-export type TodoItem = typeof todoTable.$inferSelect;
-export type TodoInsert = typeof todoTable.$inferInsert;
Index: global.d.ts
===================================================================
--- global.d.ts	(revision 66bf4fdd2573ef9770d4f2d8dc1e5a8f3f842a7c)
+++ global.d.ts	(revision c30935a5f027dac0916111525d007c79078ea267)
@@ -1,9 +1,11 @@
 import { Session } from "@auth/core/types";
-import type { db } from "./database/drizzle/db";
+import type { Database } from "./database/drizzle/db";
 
 declare module "telefunc" {
   namespace Telefunc {
     interface Context {
-      db: ReturnType<typeof db>;
+      db: Database;
+      session: Session | null;
+      request: Request;
     }
   }
@@ -13,5 +15,5 @@
   namespace Vike {
     interface PageContextServer {
-      db: ReturnType<typeof db>;
+      db: Database;
     }
   }
Index: ges/index/+Page.tsx
===================================================================
--- pages/index/+Page.tsx	(revision 66bf4fdd2573ef9770d4f2d8dc1e5a8f3f842a7c)
+++ 	(revision )
@@ -1,16 +1,0 @@
-import { Counter } from "./Counter.js";
-
-export default function Page() {
-  return (
-    <>
-      <h1>My Vike app</h1>
-      <p>This page is:</p>
-      <ul>
-        <li>Rendered to HTML.</li>
-        <li>
-          Interactive. <Counter />
-        </li>
-      </ul>
-    </>
-  );
-}
Index: ges/star-wars/@id/+Page.tsx
===================================================================
--- pages/star-wars/@id/+Page.tsx	(revision 66bf4fdd2573ef9770d4f2d8dc1e5a8f3f842a7c)
+++ 	(revision )
@@ -1,16 +1,0 @@
-import { useData } from "vike-react/useData";
-import type { Data } from "./+data.js";
-
-export default function Page() {
-  const { movie } = useData<Data>();
-  return (
-    <>
-      <h1>{movie.title}</h1>
-      Release Date: {movie.release_date}
-      <br />
-      Director: {movie.director}
-      <br />
-      Producer: {movie.producer}
-    </>
-  );
-}
Index: ges/star-wars/@id/+data.ts
===================================================================
--- pages/star-wars/@id/+data.ts	(revision 66bf4fdd2573ef9770d4f2d8dc1e5a8f3f842a7c)
+++ 	(revision )
@@ -1,31 +1,0 @@
-// https://vike.dev/data
-
-import type { PageContextServer } from "vike/types";
-import { useConfig } from "vike-react/useConfig";
-import type { MovieDetails } from "../types.js";
-
-export type Data = Awaited<ReturnType<typeof data>>;
-
-export async function data(pageContext: PageContextServer) {
-  // https://vike.dev/useConfig
-  const config = useConfig();
-
-  const response = await fetch(`https://brillout.github.io/star-wars/api/films/${pageContext.routeParams.id}.json`);
-  let movie = (await response.json()) as MovieDetails;
-
-  config({
-    // Set <title>
-    title: movie.title,
-  });
-
-  // We remove data we don't need because the data is passed to
-  // the client; we should minimize what is sent over the network.
-  movie = minimize(movie);
-
-  return { movie };
-}
-
-function minimize(movie: MovieDetails): MovieDetails {
-  const { id, title, release_date, director, producer } = movie;
-  return { id, title, release_date, director, producer };
-}
Index: ges/star-wars/index/+Page.tsx
===================================================================
--- pages/star-wars/index/+Page.tsx	(revision 66bf4fdd2573ef9770d4f2d8dc1e5a8f3f842a7c)
+++ 	(revision )
@@ -1,21 +1,0 @@
-import { useData } from "vike-react/useData";
-import type { Data } from "./+data.js";
-
-export default function Page() {
-  const { movies } = useData<Data>();
-  return (
-    <>
-      <h1>Star Wars Movies</h1>
-      <ol>
-        {movies.map(({ id, title, release_date }) => (
-          <li key={id}>
-            <a href={`/star-wars/${id}`}>{title}</a> ({release_date})
-          </li>
-        ))}
-      </ol>
-      <p>
-        Source: <a href="https://brillout.github.io/star-wars">brillout.github.io/star-wars</a>.
-      </p>
-    </>
-  );
-}
Index: ges/star-wars/index/+data.ts
===================================================================
--- pages/star-wars/index/+data.ts	(revision 66bf4fdd2573ef9770d4f2d8dc1e5a8f3f842a7c)
+++ 	(revision )
@@ -1,32 +1,0 @@
-// https://vike.dev/data
-
-import { useConfig } from "vike-react/useConfig";
-import type { Movie, MovieDetails } from "../types.js";
-
-export type Data = Awaited<ReturnType<typeof data>>;
-
-export async function data() {
-  // https://vike.dev/useConfig
-  const config = useConfig();
-
-  const response = await fetch("https://brillout.github.io/star-wars/api/films.json");
-  const moviesData = (await response.json()) as MovieDetails[];
-
-  config({
-    // Set <title>
-    title: `${moviesData.length} Star Wars Movies`,
-  });
-
-  // We remove data we don't need because the data is passed to the client; we should
-  // minimize what is sent over the network.
-  const movies = minimize(moviesData);
-
-  return { movies };
-}
-
-function minimize(movies: MovieDetails[]): Movie[] {
-  return movies.map((movie) => {
-    const { title, release_date, id } = movie;
-    return { title, release_date, id };
-  });
-}
Index: ges/star-wars/types.ts
===================================================================
--- pages/star-wars/types.ts	(revision 66bf4fdd2573ef9770d4f2d8dc1e5a8f3f842a7c)
+++ 	(revision )
@@ -1,10 +1,0 @@
-export type Movie = {
-  id: string;
-  title: string;
-  release_date: string;
-};
-
-export type MovieDetails = Movie & {
-  director: string;
-  producer: string;
-};
Index: ges/todo/+Page.tsx
===================================================================
--- pages/todo/+Page.tsx	(revision 66bf4fdd2573ef9770d4f2d8dc1e5a8f3f842a7c)
+++ 	(revision )
@@ -1,10 +1,0 @@
-import { TodoList } from "./TodoList.js";
-
-export default function Page() {
-  return (
-    <>
-      <h1>To-do List</h1>
-      <TodoList />
-    </>
-  );
-}
Index: ges/todo/+config.ts
===================================================================
--- pages/todo/+config.ts	(revision 66bf4fdd2573ef9770d4f2d8dc1e5a8f3f842a7c)
+++ 	(revision )
@@ -1,3 +1,0 @@
-export const config = {
-  prerender: false,
-};
Index: ges/todo/+data.ts
===================================================================
--- pages/todo/+data.ts	(revision 66bf4fdd2573ef9770d4f2d8dc1e5a8f3f842a7c)
+++ 	(revision )
@@ -1,12 +1,0 @@
-// https://vike.dev/data
-
-import * as drizzleQueries from "../../database/drizzle/queries/todos";
-import type { PageContextServer } from "vike/types";
-
-export type Data = Awaited<ReturnType<typeof data>>;
-
-export async function data(_pageContext: PageContextServer) {
-  const todoItemsInitial = await drizzleQueries.getAllTodos(_pageContext.db);
-
-  return { todoItemsInitial };
-}
Index: ges/todo/TodoList.telefunc.ts
===================================================================
--- pages/todo/TodoList.telefunc.ts	(revision 66bf4fdd2573ef9770d4f2d8dc1e5a8f3f842a7c)
+++ 	(revision )
@@ -1,9 +1,0 @@
-// We use Telefunc (https://telefunc.com) for data mutations.
-
-import * as drizzleQueries from "../../database/drizzle/queries/todos";
-import { getContext } from "telefunc";
-
-export async function onNewTodo({ text }: { text: string }) {
-  const context = getContext();
-  await drizzleQueries.insertTodo(context.db, text);
-}
Index: ges/todo/TodoList.tsx
===================================================================
--- pages/todo/TodoList.tsx	(revision 66bf4fdd2573ef9770d4f2d8dc1e5a8f3f842a7c)
+++ 	(revision )
@@ -1,36 +1,0 @@
-import type { Data } from "./+data";
-import { onNewTodo } from "./TodoList.telefunc";
-import { useState } from "react";
-import { useData } from "vike-react/useData";
-
-export function TodoList() {
-  const { todoItemsInitial } = useData<Data>();
-  const [todoItems, setTodoItems] = useState<{ text: string }[]>(todoItemsInitial);
-  const [newTodo, setNewTodo] = useState("");
-  return (
-    <>
-      <ul>
-        {todoItems.map((todoItem, index) => (
-          // biome-ignore lint: example
-          <li key={index}>{todoItem.text}</li>
-        ))}
-      </ul>
-      <div>
-        <form
-          onSubmit={async (ev) => {
-            ev.preventDefault();
-
-            const text = newTodo;
-            setTodoItems((prev) => [...prev, { text }]);
-            setNewTodo("");
-
-            await onNewTodo({ text });
-          }}
-        >
-          <input type="text" onChange={(ev) => setNewTodo(ev.target.value)} value={newTodo} />
-          <button type="submit">Add to-do</button>
-        </form>
-      </div>
-    </>
-  );
-}
Index: server/entry.ts
===================================================================
--- server/entry.ts	(revision 66bf4fdd2573ef9770d4f2d8dc1e5a8f3f842a7c)
+++ server/entry.ts	(revision c30935a5f027dac0916111525d007c79078ea267)
@@ -2,5 +2,5 @@
 import { authjsHandler, authjsSessionMiddleware } from "./authjs-handler";
 import { dbMiddleware } from "./db-middleware";
-import { telefuncHandler } from "./telefunc-handler";
+import telefuncHandler from "./telefunc-handler";
 import { apply, serve } from "@photonjs/hono";
 import { Hono } from "hono";
Index: server/telefunc-handler.ts
===================================================================
--- server/telefunc-handler.ts	(revision 66bf4fdd2573ef9770d4f2d8dc1e5a8f3f842a7c)
+++ server/telefunc-handler.ts	(revision c30935a5f027dac0916111525d007c79078ea267)
@@ -2,6 +2,7 @@
 import { enhance, type UniversalHandler } from "@universal-middleware/core";
 import { telefunc } from "telefunc";
+import type { Session } from "@auth/core/types";
 
-export const telefuncHandler: UniversalHandler = enhance(
+const telefuncHandler: UniversalHandler = enhance(
   async (request, context, runtime) => {
     const httpResponse = await telefunc({
@@ -10,7 +11,9 @@
       body: await request.text(),
       context: {
-        ...(context as { db: Database }),
-        ...runtime,
-      },
+          ...(context as { db: Database; session?: Session | null }),
+          ...runtime,
+          session: (context as { session?: Session | null }).session ?? null,
+          request,
+      }
     });
     const { body, statusCode, contentType } = httpResponse;
@@ -29,2 +32,3 @@
   },
 );
+export default telefuncHandler
Index: server/telefunc/ctx.ts
===================================================================
--- server/telefunc/ctx.ts	(revision c30935a5f027dac0916111525d007c79078ea267)
+++ server/telefunc/ctx.ts	(revision c30935a5f027dac0916111525d007c79078ea267)
@@ -0,0 +1,39 @@
+import { Abort, getContext } from "telefunc";
+import * as drizzleQueries from "../../database/drizzle/queries";
+
+export function ctx() {
+    return getContext();
+}
+
+export function parseSessionUserId(sessionUserId: unknown): number | null {
+    if (typeof sessionUserId !== "string" && typeof sessionUserId !== "number") return null;
+    const n = Number(sessionUserId);
+    return Number.isInteger(n) && n > 0 ? n : null;
+}
+
+export function requireUser() {
+    const c = ctx();
+    const userId = parseSessionUserId(c.session?.user?.id);
+    if (!userId) throw Abort();
+    return { c, userId };
+}
+
+export function getAuthState() {
+    const c = ctx();
+    const userId = parseSessionUserId(c.session?.user?.id);
+    return {
+        isLoggedIn: Boolean(userId),
+        userId: userId ?? null,
+        username: c.session?.user?.name ?? null,
+        session: c.session,
+    };
+}
+
+export async function requireAdmin() {
+    const { c, userId } = requireUser();
+
+    const isAdmin = await drizzleQueries.isAdmin(c.db, userId);
+
+    if (!isAdmin) throw Abort();
+    return { c, userId };
+}
