# 11. Backend Architecture ## Server Functions Organization ``` src/server/ ├── functions/ │ ├── auth.ts # Authentication functions │ ├── characters.ts # Character CRUD │ ├── accounts.ts # Account CRUD │ ├── teams.ts # Team management │ ├── progressions.ts # Progression tracking │ └── dofusdb.ts # DofusDB sync │ ├── middleware/ │ └── auth.ts # Authentication middleware │ └── index.ts # Export all functions ``` ## Server Function Example ```typescript // src/server/functions/characters.ts import { createServerFn } from "@tanstack/react-start/server"; import { z } from "zod"; import { db } from "@/lib/server/db"; import { requireAuth } from "@/server/middleware/auth"; // Schemas const createCharacterSchema = z.object({ name: z.string().min(2).max(50), level: z.number().int().min(1).max(200), classId: z.number().int().positive(), className: z.string(), serverId: z.number().int().positive(), serverName: z.string(), accountId: z.string().cuid(), }); const characterFiltersSchema = z.object({ search: z.string().optional(), classIds: z.array(z.number()).optional(), serverIds: z.array(z.number()).optional(), accountIds: z.array(z.string()).optional(), page: z.number().int().positive().default(1), limit: z.number().int().min(1).max(100).default(20), }); // Functions export const getCharacters = createServerFn({ method: "GET" }) .validator((data: unknown) => characterFiltersSchema.parse(data)) .handler(async ({ data }) => { const session = await requireAuth(); const { search, classIds, serverIds, accountIds, page, limit } = data; const where = { account: { userId: session.userId }, ...(search && { name: { contains: search, mode: "insensitive" as const }, }), ...(classIds?.length && { classId: { in: classIds } }), ...(serverIds?.length && { serverId: { in: serverIds } }), ...(accountIds?.length && { accountId: { in: accountIds } }), }; const [characters, total] = await Promise.all([ db.character.findMany({ where, include: { account: { select: { id: true, name: true } } }, skip: (page - 1) * limit, take: limit, orderBy: { name: "asc" }, }), db.character.count({ where }), ]); return { characters, pagination: { page, limit, total, totalPages: Math.ceil(total / limit), }, }; }); export const createCharacter = createServerFn({ method: "POST" }) .validator((data: unknown) => createCharacterSchema.parse(data)) .handler(async ({ data }) => { const session = await requireAuth(); // Verify account belongs to user const account = await db.account.findFirst({ where: { id: data.accountId, userId: session.userId }, }); if (!account) { throw new Error("Account not found"); } return db.character.create({ data }); }); export const bulkDeleteCharacters = createServerFn({ method: "POST" }) .validator((data: unknown) => z.object({ ids: z.array(z.string().cuid()) }).parse(data), ) .handler(async ({ data }) => { const session = await requireAuth(); const result = await db.character.deleteMany({ where: { id: { in: data.ids }, account: { userId: session.userId }, }, }); return { deleted: result.count }; }); ``` ## Authentication Middleware ```typescript // src/server/middleware/auth.ts import { getWebRequest } from "@tanstack/react-start/server"; import { db } from "@/lib/server/db"; interface Session { userId: string; sessionId: string; } export async function requireAuth(): Promise { const request = getWebRequest(); const sessionId = request.headers .get("cookie") ?.split(";") .find((c) => c.trim().startsWith("session=")) ?.split("=")[1]; if (!sessionId) { throw new Error("Unauthorized"); } const session = await db.session.findUnique({ where: { id: sessionId }, include: { user: true }, }); if (!session || session.expiresAt < new Date()) { throw new Error("Session expired"); } return { userId: session.userId, sessionId: session.id, }; } export async function getOptionalAuth(): Promise { try { return await requireAuth(); } catch { return null; } } ``` ## Caching Strategy ```typescript // src/lib/server/cache.ts import NodeCache from "node-cache"; // Different TTLs for different data types const caches = { // Reference data (rarely changes) reference: new NodeCache({ stdTTL: 3600, checkperiod: 600 }), // 1 hour // User data (changes more frequently) user: new NodeCache({ stdTTL: 300, checkperiod: 60 }), // 5 minutes }; export const referenceCache = { get: (key: string): T | undefined => caches.reference.get(key), set: (key: string, value: T): boolean => caches.reference.set(key, value), del: (key: string): number => caches.reference.del(key), flush: (): void => caches.reference.flushAll(), }; export const userCache = { get: (key: string): T | undefined => caches.user.get(key), set: (key: string, value: T): boolean => caches.user.set(key, value), del: (key: string | string[]): number => caches.user.del(key), flush: (): void => caches.user.flushAll(), // Helper to invalidate all cache for a user invalidateUser: (userId: string): void => { const keys = caches.user .keys() .filter((k) => k.startsWith(`user:${userId}:`)); caches.user.del(keys); }, }; ``` ---