coming from React, landing on SvelteKit
This is the newest tool in the whole stack for me, and also the one doing the most jobs at once. SvelteKit renders pages on the server, handles form submissions, and proxies API calls to the two Go services, all from one Node process. Coming from a mostly React and MERN-stack background before this, the idea that a page load and a form submit both live in the same file, with a load function and an actions object sitting right next to each other, took some getting used to. It's not a single-page app calling a REST API from client-side JavaScript, most of the important stuff here never touches the browser's own script engine at all.

the browser never actually holds the token
This is the one architectural decision I'm most protective of in this whole project. The access token that proves who you are lives in an httponly cookie, which means no JavaScript running in the browser, not mine, not a browser extension, not an XSS payload if one ever slipped through, can read it. But it still has to get from that cookie into the Authorization header the Go services expect, and that translation happens entirely on the server, inside SvelteKit.
Every request to the backend goes through a catch-all proxy route on the frontend server. The browser calls /api/whatever on the frontend's own origin, SvelteKit reads the cookie server-side and attaches it as a real bearer token before forwarding the request onward:
export async function forwardRequest(
event: RequestEvent,
target: URL,
extraHeaders: Record<string, string> = {}
): Promise<Response> {
const allowed = getAllowedOrigins();
if (!isAllowedTarget(target, allowed)) {
throw new Error(`target origin not in allowed origins: ${target.origin}`);
}
const headers = new Headers(extraHeaders);
const contentType = event.request.headers.get('content-type');
if (contentType) headers.set('content-type', contentType);
...
return fetch(target, init);
}That isAllowedTarget check against a fixed allowlist of internal origins is there so a bug somewhere upstream can't get tricked into forwarding a request to some arbitrary host with our auth headers stapled to it, basically SSRF protection for something that on the surface just looks like an ordinary proxy function.
session state without ever verifying, on purpose
The other place tokens confused me for a while: hooks.server.ts runs on every single request and decides who's logged in for that request, but it only decodes the JWT, it doesn't verify the signature at all.
export const handle: Handle = async ({ event, resolve }) => {
event.locals.user = null;
let sessionFromAccessToken = false;
const accessToken = event.cookies.get('access_token');
if (accessToken) {
const claims = decodeAccessToken(accessToken);
if (claims && !isExpired(claims)) {
event.locals.user = claimsToLocalsUser(claims);
sessionFromAccessToken = true;
}
}
if (!sessionFromAccessToken) {
const refreshToken = event.cookies.get('refresh_token');
if (refreshToken) {
const response = await event.fetch('/auth/refresh', { method: 'POST' });
...
}
}
...
};Reads like a security hole the first time you look at it, and it would be, if locals.user were ever treated as proof of anything. It's not. It only decides what the page renders: whether the admin link shows up in the nav, whether a form shows "log out" instead of "log in." Every actual write still goes through the Go services, and they're the ones that verify the signature against auth's public keys before doing anything with it. If someone forged a cookie claiming to be admin, the UI would happily show them the admin nav link, and then every single request they made would get rejected by the real check on the other end anyway. Cosmetic on this side, load-bearing on that one.
a form action, not a fetch call
Login is a plain HTML form, submitted as a SvelteKit form action, which means it keeps working even with JavaScript turned off:
export const actions: Actions = {
default: async ({ request, fetch, url }) => {
const form = await request.formData();
const username = String(form.get('username') ?? '');
const password = String(form.get('password') ?? '');
let response: Response;
try {
response = await fetch('/auth/login', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ username, password })
});
} catch (err) {
const apiError = await parseApiError(err);
return fail(502, { error: apiError.message, requestId: apiError.requestId, username });
}
if (!response.ok) {
const apiError = await parseApiError(response);
return fail(response.status, { error: apiError.message, requestId: apiError.requestId, username });
}
throw redirect(303, safeRedirectTarget(url.searchParams.get('redirectTo')));
}
};That fetch call is SvelteKit's own server-side fetch, running inside the form action, hitting the internal /auth/login route which is itself the proxy from above. The browser never sees any of this except a normal form POST and a redirect at the end.
First time I wired this up I kept trying to call the auth service straight from a <script> in the component like I would have in React:
<script lang="ts">
// what I tried first, straight out of a React habit
async function login(username: string, password: string) {
const res = await fetch('http://localhost:8081/auth/login', {
method: 'POST',
body: JSON.stringify({ username, password })
});
}
</script>Kept hitting CORS, since that's a browser calling a completely different origin directly, and eventually accepted that no, that's not how this works here, the server does it instead.
markdown that can't run scripts
Post content and comments both get written as markdown and rendered to HTML, which means user input eventually becomes real DOM on the page. That's the textbook setup for stored XSS if you're not careful about it. Rendering goes through marked and then straight through DOMPurify with an explicit allowlist, not a blocklist:
const POST_ALLOWED_TAGS = [
'p', 'br', 'strong', 'em', 'del', 'a', 'ul', 'ol', 'li',
'h2', 'h3', 'h4', 'blockquote', 'code', 'pre', 'span',
'img', 'video', 'hr', 'table', 'thead', 'tbody', 'tr', 'th', 'td'
];
const COMMENT_ALLOWED_TAGS = ['p', 'br', 'a', 'strong', 'em', 'code'];
const COMMENT_ALLOWED_ATTR = ['href'];Posts get a wider allowlist than comments, because only admins write posts, blog's own API rejects anyone else at the create/update endpoint, so I trust that content more. Comments come from anonymous strangers on the internet, so they get the smallest list, no images, no styling, barely more than a paragraph and a link. Even the video tag gets an extra check on top of the allowlist, a hook that only lets a <video> element keep its src if that URL's origin matches our own media host, so a comment can't point a video tag at some other domain entirely.
Small system, but it's the one place I felt like I actually understood a real security principle (allowlist beats blocklist, every time) instead of just being told it in a lecture and nodding along.