This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
bun dev # start dev server
bun build # production build
bun preview # preview production build
bun check # type-check via svelte-check + tsconfig.json
bun check:watch # type-check in watch mode
bun lint # ESLint
bun test # Playwright e2e testsBefore marking any task as complete, always run:
bun lint # must pass with zero errors
bun check # must pass with zero errorsFor deployment:
make build # build Docker image and push to registry.deploys.app
make deploy # build and deploy to deploys-app projectmain is protected — all changes must go through a pull request; never commit or push directly to main. Branch off the latest main (git checkout main && git pull), make changes, then open a PR. Merging a PR auto-deletes its branch, which can silently leave your local checkout on main — so verify the current branch before committing or pushing.
UI changes must include screenshots in the PR description. Any PR that adds or changes user-visible UI (a page, component, modal, layout, or styling) must show the result visually in the PR body — not just describe it. Capture the affected screens with the mock + Playwright (bun dev:mock, then screenshot via @playwright/test), covering the relevant states (e.g. empty vs populated, a toggle on/off, light/dark when it matters); a before/after is ideal for changes to existing screens. Attach mechanism: store the images on the dedicated screenshots branch — a long-lived branch that is never merged into main, so the binaries stay out of the source tree — and embed them in the PR body via raw URLs. Name each file by PR number (<PR#>-before.png, <PR#>-after.png, or <PR#>-<label>.png). Because the filename needs the PR number, open the PR first, then push the screenshots and edit the body to add the links. Reference them as https://raw.githubusercontent.com/deploys-app/console/screenshots/<file> — e.g. a side-by-side before/after:
|  |  |
| --- | --- |To publish the images (after PR #<N> exists):
git fetch origin screenshots
git worktree add ../.worktrees/console-screenshots screenshots
cp before.png ../.worktrees/console-screenshots/<N>-before.png
cp after.png ../.worktrees/console-screenshots/<N>-after.png
cd ../.worktrees/console-screenshots && git add . && git commit -m "screenshots: console#<N> before/after" && git push origin screenshots
git worktree remove ../.worktrees/console-screenshotsNever commit screenshots into the source tree on main just to embed them.
This is a SvelteKit 2 console for deploys.app, a deployment platform. It uses Svelte 5 runes ($state, $derived, $props) and is deployed on Node.js via @sveltejs/adapter-node (served over h2c by the custom server.js entrypoint — see the Dockerfile).
src/routes/
auth/ # sign-in / callback / sign-out (unauthenticated)
(auth)/ # layout group — redirects to /auth/signin if no token
(project)/ # layout group — requires ?project= param
audit-log/ deployment/ disk/ domain/ dropbox/ email/ env-group/
pull-secret/ registry/ role/ route/
service-account/ workload-identity/
billing/
project/ # project selection & creation
api/[fn]/+server.js # server-side API proxy (adds auth header, forwards to API_ENDPOINT)
The (auth) and (project) parentheses are SvelteKit route groups — they share layouts without affecting the URL.
All API calls go through a single proxy: POST /api/{fnName}.
- Client side:
src/lib/api/index.ts—api.invoke(fnName, args, fetch)posts to the local proxy. - Server side:
src/routes/api/[fn]/+server.ts— reads token from cookies, forwards toenv.API_ENDPOINT(https://api.deploys.app) withAuthorization: bearer {token}. - Cache invalidation:
api.invalidate(fn)/api.intervalInvalidate(cb, interval)for polling. - Error handling:
api: unauthorized→ reloads page;api: forbidden→error.forbidden;api: validate error→error.validate[];api: * not found→error.notFound. - Mock mode (offline dev):
bun dev:mock(setsMOCK_API=1). Both proxies (api/[fn]andapi/registry/[fn]) short-circuit to static fixtures insrc/lib/server/mock.ts, skipping the token check — no real backend or OAuth sign-in needed. Add/adjust a fixture inmock.ts(handlersmap) when adding a new API call; unknown functions log a warning and return an emptyokresponse.
All API types are defined in src/types/api.d.ts using TypeScript namespaces (Api.Profile, Api.Response<T>, Api.Deployment, etc.).
No global store. Data flows via SvelteKit's load() functions:
+layout.server.tsfor server-loaded data (auth token, user profile)+page.ts/+layout.tsfor client-side data fetching- Svelte 5 runes (
$state,$derived) for component-local state
OAuth2 against auth.deploys.app. Token stored in httpOnly cookies. src/hooks.server.ts injects the token into every /api/* request and persists the selected project + theme in cookies. The theme cookie (dark | light) is rendered into the <html class="…"> placeholder via transformPageChunk so the first paint already matches.
Tailwind CSS v4 via @tailwindcss/vite. The single entry stylesheet is src/style/app.css (imported once from src/routes/(auth)/+layout.svelte and aliased as $style).
app.css is the source of truth for design tokens. It contains:
:rootand.darkblocks that hold the raw HSL/spacing/typography/shadow CSS variables.- A
@theme inline { … }block that exposes those variables to Tailwind as--color-*/--font-*tokens, so utilities likebg-primary,text-content,border-line,text-positive/80resolve dynamically per theme. - An
@layer baseblock with element resets and typography rules. - An
@layer componentsblock defining the project's component classes:panel(withis-level-{200,300,400}),button(withis-variant-{secondary,tertiary,negative,positive,accent,underline},is-size-{small,large},is-icon-{left,right},is-loading),input/select/textarea(with their[readonly]/[disabled]and.-has-icon-{left,right}states),field,checkbox,link,label,breadcrumb(-item),tabs(withis-variant-underline),table(-container)(withis-variant-compact,is-collapse),dropdown/menu(withis-card/is-compact),modal(-panel/-close). These replace the removed nomimono dependency — keep using the existing class names rather than reinventing them.
Dark mode is the Tailwind class strategy: <html class="dark"> activates dark utilities. The dark variant is registered with @custom-variant dark (&:where(.dark, .dark *)). The theme toggle in Navbar.svelte writes the cookie and toggles the class on document.documentElement.
Conventions when adding UI:
- Reach for Tailwind utilities first (
flex,gap-3,mt-6,text-content/70,lg:grid-cols-2, etc.). - Use the project component classes (
button,input,panel,field,table,link, etc.) — don't re-derive them with bare utilities. - Reference design tokens via the Tailwind utility (
text-primary) or the underlying CSS var (hsl(var(--hsl-primary) / 0.12)) inside scoped<style>blocks; both stay in sync with light/dark. - Scoped
<style>blocks are plain CSS (nolang="scss", no SCSS nesting) —sassis no longer a dependency.
Two complementary patterns:
src/lib/modal/index.ts—confirm/error/success/prompt, backed by a single native<dialog>(src/lib/modal/store.svelte.tsholds the rune state;Modal.svelteis the host, mounted once in(auth)/+layout.svelte). The dialog closes via plain JS (dialogEl.close()) — we deliberately avoid the Invoker Commands API (command="close") while iOS support is limited. Use these for transient alerts, confirmations, and simple text prompts rather than inlinealert/confirm.promptsupportsrequireMatchfor type-to-confirm gates.- In-page Svelte modal components — for showing structured or user-provided content, build a small component using the
modal/modal-panel/modal-closeclasses with an exportedopen()and local state (seeModalSelectProject.svelte,EnvGroupModal.svelte). Values render through Svelte interpolation (auto-escaped) rather than hand-built HTML strings. Bindaria-hiddento the open state so the dialog is exposed to assistive tech only while open.
The codebase is TypeScript (fully strict: strict + noImplicitAny), checked via svelte-check against tsconfig.json. The JS-with-JSDoc → TS migration is complete: every file under src/ is .ts or a .svelte with <script lang="ts">. (allowJs stays on only for the root tooling configs like vite.config.js/svelte.config.js; checkJs is off so those don't need strict typing.)
- Write new code in TypeScript:
.tsmodules and.sveltecomponents with<script lang="ts">, typing props via aninterface Props+const { … }: Props = $props(). - Type SvelteKit
loadfunctions withPageLoad/LayoutLoad(etc.) imported from./$types. - Shared API types live in
src/types/api.d.tsunder the globalApinamespace (no import needed). - When converting an existing file, lift its JSDoc
@type/@typedefannotations into real TS types and delete the JSDoc. - ESLint parses
.tsandlang="ts"blocks via@typescript-eslint/parser(seeeslint.config.js);bun lintandbun checkmust both stay green.
Charts are rendered in-house as plain SVG — there is no charting dependency (Highcharts was removed). The pieces live under src/lib/charts/ and src/lib/components/:
src/lib/charts/util.ts— pure helpers shared by the chart components:niceScale(round axis ticks),formatBytes(binary Ki/Mi/Gi),formatNumber,linePath/areaPath(straight + Catmull-Rom spline), and thepaletteof token-based colors.LineChart.svelte— responsive SVG line/area engine with a time or category x-axis, crosshair + floating tooltip, optional legend, and a draw-in animation. Used byChart.svelte(metric panels) and the billing report.Chart.svelte— titled metric panel wrappingLineChart; takes the platform metric shape ({prefix, lines:[{name, points:[[unixSec, val]]}], dashStyle?, color?}).WafActivityChart.svelte— stacked-column engine for WAF activity (per-action segments over time).
Colors must stay CSS-var strings applied via inline style (style:fill, style:stroke, style:stop-color) — never SVG presentation attributes, which don't evaluate var(). Done this way, charts recolor instantly on theme toggle with no re-render. Keep new chart code dependency-free and on these primitives.