Utilities
Honeycomb organizes its utility code across several locations. Client-safe helpers live in src/lib/utils.ts and src/lib/utils/, server-only code lives in src/lib/server/, reactive hooks live in src/lib/hooks/, and feature-scoped logic lives in src/lib/modules/.
cn() — class name merging
Section titled “cn() — class name merging”The most commonly used utility. See the Styling page for full details.
import { clsx, type ClassValue } from "clsx";import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs));}Type helpers
Section titled “Type helpers”src/lib/utils.ts also exports several type utilities used by shadcn-svelte components to strip slot-related props when composing wrapper components:
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, "child"> : T;export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, "children"> : T;export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };| Type | Purpose |
|---|---|
WithoutChild<T> | Removes the child snippet prop so a wrapper can supply its own |
WithoutChildren<T> | Removes the children snippet prop |
WithoutChildrenOrChild<T> | Removes both child and children |
WithElementRef<T, U> | Adds an optional ref prop typed to a specific HTML element |
Usage example in a wrapper component:
<script lang="ts"> import type { WithoutChildrenOrChild, WithElementRef } from "$lib/utils"; import type { ButtonProps } from "bits-ui";
let { ref, ...rest }: WithElementRef<WithoutChildrenOrChild<ButtonProps>> = $props();</script>CSRF protection
Section titled “CSRF protection”File: src/lib/utils/csrf.ts
SvelteKit form actions have built-in CSRF protection, but +server.ts API endpoints do not. This module provides origin-based CSRF validation for those routes.
csrfProtection(event)
Section titled “csrfProtection(event)”The primary guard. Call it at the top of any +server.ts handler that mutates data:
import { csrfProtection } from "$lib/utils/csrf";
export async function POST(event) { const blocked = csrfProtection(event); if (blocked) return blocked; // 403 JSON response
// safe to proceed...}Returns null if the request is valid, or a 403 Response with a JSON error body if it fails.
verifyCsrfToken(event)
Section titled “verifyCsrfToken(event)”Lower-level boolean check. Skips GET/HEAD/OPTIONS (safe methods) and validates that the Origin header (or Referer fallback) matches event.url.host:
export function verifyCsrfToken(event: RequestEvent): booleanisSameOrigin(event)
Section titled “isSameOrigin(event)”Simple origin-match check useful for additional security layers:
export function isSameOrigin(event: RequestEvent): booleanServer utilities
Section titled “Server utilities”Files under src/lib/server/ are only importable in server-side contexts (+page.server.ts, +server.ts, hooks.server.ts). SvelteKit enforces this at build time.
Route guards
Section titled “Route guards”File: src/lib/server/guards.ts
Centralized auth and access control logic consumed by hooks.server.ts. Individual +page.server.ts files should not contain auth redirects — the hook handles everything before they run.
Route classifications
Section titled “Route classifications”| Constant / Function | Description |
|---|---|
PUBLIC_AUTH_ROUTES | Auth pages (/sign-in, /sign-up, etc.) — authenticated users are redirected away |
PUBLIC_ROUTES | Fully public paths (callbacks, webhooks) — no redirects either way |
AUTHENTICATED_REDIRECT | Where logged-in users go when hitting a public auth route (/feed) |
UNAUTHENTICATED_REDIRECT | Where anonymous users go when hitting a protected route (/sign-in) |
isProtectedRoute(path) | Returns true for any path requiring authentication |
isAdminRoute(path) | Returns true for /admin paths |
requiresOnboarding(path) | Returns true for protected paths that need completed onboarding |
isLandingPage(path) | Returns true for / only |
Protected route groups
Section titled “Protected route groups”The following prefixes are protected (require authentication):
/feed, /post/, /profile, /explore, /messages, /notifications,/bookmarks, /settings, /wallet, /marketplace, /jobs, /business,/admin, /store/, /drafts, /onboardingAPI routes under /api/ are also protected unless they appear in PUBLIC_ROUTES (e.g., the Stripe webhook).
Onboarding gate
Section titled “Onboarding gate”requiresOnboarding(path) returns false for /onboarding itself, /sign-out, and /api/ routes so that users mid-onboarding can still access the flow and API calls work normally.
Safe redirects
Section titled “Safe redirects”File: src/lib/server/safe-redirect.ts
Prevents open-redirect vulnerabilities when processing ?next= query parameters:
export function safeInternalRedirectPath( baseUrl: URL, next: string | null | undefined, fallback = "/",): stringRules enforced:
- The path must start with
/ - Scheme-relative URLs (
//evil.com) are rejected - Backslashes are rejected
- The resolved origin must match
baseUrl.origin - Returns the
fallback("/"by default) if any check fails
import { safeInternalRedirectPath } from "$lib/server/safe-redirect";
const next = url.searchParams.get("next");const target = safeInternalRedirectPath(url, next, "/feed");redirect(303, target);Stripe server module
Section titled “Stripe server module”File: src/lib/server/stripe.ts
Initializes the Stripe SDK with the secret key from environment variables and exports shared billing constants:
import Stripe from "stripe";import { STRIPE_SECRET_KEY } from "$env/static/private";
export const stripe = new Stripe(STRIPE_SECRET_KEY);export const PLATFORM_FEE_PERCENT = 5;
export const PLANS = { starter: { name: "Starter", priceMonthly: 9, features: ["Business profile", "1 extension", "Basic analytics", "Standard support"], }, pro: { name: "Pro", priceMonthly: 29, features: [ "Business profile", "Unlimited extensions", "Advanced analytics", "Priority support", "Custom domain", "API access", ], },} as const;
export type PlanId = keyof typeof PLANS; // "starter" | "pro"IsMobile
Section titled “IsMobile”File: src/lib/hooks/is-mobile.svelte.ts
A reactive media-query hook built on Svelte 5’s MediaQuery rune class:
import { MediaQuery } from "svelte/reactivity";
const DEFAULT_MOBILE_BREAKPOINT = 768;
export class IsMobile extends MediaQuery { constructor(breakpoint: number = DEFAULT_MOBILE_BREAKPOINT) { super(`max-width: ${breakpoint - 1}px`); }}Usage:
<script> import { IsMobile } from "$lib/hooks/is-mobile.svelte";
const isMobile = new IsMobile();</script>
{#if isMobile.current} <MobileNav />{:else} <DesktopNav />{/if}The default breakpoint is 768px (matching Tailwind’s md). Pass a custom value to override: new IsMobile(1024).
Module system
Section titled “Module system”Feature-specific code is organized under src/lib/modules/<module>/ with a standardized structure. Each module can contain schemas/, types/, components/, stores/, utils/, and a constants.ts.
Current modules:
auth, explore, honeycomb, layout, marketplace, messaging,notifications, onboarding, post, profile, shared,sitebuilder, social, stories, walletImport convention
Section titled “Import convention”Always import from the module barrel, never from internal files:
import { LoginForm, signInSchema } from "$lib/modules/auth";import LoginForm from "$lib/modules/auth/components/login-form.svelte";Key rules
Section titled “Key rules”- Barrel files — every module and subfolder must have an
index.tsre-exporting its public API. - Schemas — one Zod schema per file under
schemas/. - Components — feature-scoped Svelte components. Shared UI primitives stay in
$lib/components/ui/. - Stores — Svelte 5 rune state in
.svelte.tsfiles. Only create when client-side state is needed. - Server code stays in
src/routes/— modules contain only client-importable code. Form actions, API endpoints, and server loaders do not belong in modules.