Code Patterns
This page documents the coding conventions and patterns used across the tripplan.ing codebase. Follow these when contributing to maintain consistency.
Svelte 5
tripplan.ing uses Svelte 5 with runes. Do not use legacy reactive syntax ($:, export let).
Props
<script lang="ts">
let { value = '', onchange }: {
value?: string;
onchange?: (v: string) => void;
} = $props();
</script>Reactive state
<script lang="ts">
let count = $state(0);
let doubled = $derived(count * 2);
$effect(() => {
console.log('count changed:', count);
});
</script>Key rules
- Use
$props()for component inputs, notexport let - Use
$state()for local reactive state - Use
$derived()for computed values (replaces$:assignments) - Use
$effect()sparingly — prefer derived values over effects - Destructure props with defaults in the
$props()call
Server-side patterns
Accessing the runtime environment
Always use getRuntimeEnv() to get database, KV, and blob handles:
import { getRuntimeEnv } from '$lib/server/runtime/index.js';
export const load: PageServerLoad = async ({ platform, locals }) => {
const env = await getRuntimeEnv(platform);
const db = env.db;
const eventId = locals.eventId;
// ...
};Never import runtime implementations directly — always go through the index.ts entry point.
Form actions
Use SvelteKit form actions for mutations:
export const actions = {
default: async ({ request, platform, locals }) => {
const env = await getRuntimeEnv(platform);
const formData = await request.formData();
const name = formData.get('name') as string;
// Validate
if (!name?.trim()) {
return fail(400, { error: 'Name is required' });
}
// Mutate
await env.db.insert(table).values({ ... });
return { success: true };
}
};Error handling
Use SvelteKit's error() and redirect():
import { error, redirect } from '@sveltejs/kit';
if (!item) throw error(404, 'Not found');
if (!authorized) throw redirect(302, '/auth');Import order
Follow this order, separated by blank lines:
// 1. SvelteKit
import { error, redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
// 2. External packages
import { eq, and } from 'drizzle-orm';
// 3. Server-side lib imports
import { getRuntimeEnv } from '$lib/server/runtime/index.js';
import { rsvps } from '$lib/server/db/schema';
// 4. Shared lib imports
import type { EventConfig } from '$lib/types';
// 5. Relative imports
import { formatDate } from './utils';Cloudflare Workers constraints
Cloudflare Workers run on V8, not Node.js. These constraints are critical for production:
No Node.js APIs
Do not use:
cryptomodule → usecrypto.subtle(Web Crypto API)fs,path,os→ not availableBuffer→ useUint8Array/TextEncoderhttp/httpsmodules → usefetch()
Stripe
Must use the fetch-based HTTP client:
import Stripe from 'stripe';
const stripe = new Stripe(key, {
httpClient: Stripe.createFetchHttpClient()
});For webhook verification, use SubtleCryptoProvider:
import { SubtleCryptoProvider } from 'stripe';
const event = await stripe.webhooks.constructEventAsync(
body, signature, secret,
undefined,
new SubtleCryptoProvider()
);Mailgun
Use raw fetch() — no SDK:
await fetch(`https://api.mailgun.net/v3/${domain}/messages`, {
method: 'POST',
headers: {
Authorization: `Basic ${btoa(`api:${apiKey}`)}`,
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({ from, to, subject, text })
});Dynamic imports
The Node runtime (node.ts) is loaded via dynamic import() to prevent better-sqlite3 from being bundled into the Worker:
if (platform?.env) {
return getCloudflareRuntime(platform.env);
}
const { getNodeRuntime } = await import('./node.js'); // Dynamic import
return await getNodeRuntime();Database conventions
Queries always scope by event_id
// Correct
await db.select().from(rsvps).where(
and(eq(rsvps.eventId, eventId), eq(rsvps.status, 'confirmed'))
);
// Wrong — leaks data across events
await db.select().from(rsvps).where(eq(rsvps.status, 'confirmed'));IDs are UUID strings
await db.insert(rsvps).values({
id: crypto.randomUUID(),
eventId,
// ...
});Timestamps are ISO strings
createdAt: new Date().toISOString()Naming conventions
Files
| Type | Convention | Example |
|---|---|---|
| Svelte components | PascalCase | PhotoCard.svelte |
| TypeScript lib files | camelCase or kebab-case | settings.ts, resolve-event.ts |
| Routes | Lowercase with hyphens | admin/custom-fields/ |
Variables
| Context | Convention | Example |
|---|---|---|
| TypeScript/JavaScript | camelCase | contactEmail, rsvpId |
| Database columns | snake_case | contact_email, rsvp_id |
| Environment variables | SCREAMING_SNAKE_CASE | STRIPE_SECRET_KEY |
| CSS custom properties | --kebab-case | --color-primary |
Functions
| Pattern | Convention | Example |
|---|---|---|
| Actions/handlers | verbNoun | handleSubmit, createSession |
| Boolean getters | isX, hasX, canX | isLocalDev, hasPermission |
TypeScript
- No
any— useunknownand narrow with type guards - No non-null assertions (
!) — handle nullability explicitly - Explicit return types on exported functions
- Use
typefor unions and function types,interfacefor object shapes - Export shared types from
$lib/types.ts
Tailwind CSS
Class order
Follow this order: layout → sizing → typography → visual → interactive:
<div class="flex items-center gap-4 p-4 text-sm font-medium bg-white rounded-lg shadow hover:shadow-md">Theme colors
Use semantic color names, not raw Tailwind colors:
<!-- Correct -->
<div class="bg-primary text-white">
<!-- Avoid -->
<div class="bg-blue-600 text-white">Theme colors (--color-primary, --color-accent) are set dynamically from event settings in +layout.svelte.
Related pages
- Architecture — how modules connect
- Database — Drizzle schema and migration patterns
- Monorepo Layout — import boundaries and package rules