Skip to content

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/.

The most commonly used utility. See the Styling page for full details.

src/lib/utils.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

src/lib/utils.ts also exports several type utilities used by shadcn-svelte components to strip slot-related props when composing wrapper components:

src/lib/utils.ts
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 };
TypePurpose
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>

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.

The primary guard. Call it at the top of any +server.ts handler that mutates data:

+server.ts
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.

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): boolean

Simple origin-match check useful for additional security layers:

export function isSameOrigin(event: RequestEvent): boolean

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.

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.

Constant / FunctionDescription
PUBLIC_AUTH_ROUTESAuth pages (/sign-in, /sign-up, etc.) — authenticated users are redirected away
PUBLIC_ROUTESFully public paths (callbacks, webhooks) — no redirects either way
AUTHENTICATED_REDIRECTWhere logged-in users go when hitting a public auth route (/feed)
UNAUTHENTICATED_REDIRECTWhere 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

The following prefixes are protected (require authentication):

/feed, /post/, /profile, /explore, /messages, /notifications,
/bookmarks, /settings, /wallet, /marketplace, /jobs, /business,
/admin, /store/, /drafts, /onboarding

API routes under /api/ are also protected unless they appear in PUBLIC_ROUTES (e.g., the Stripe webhook).

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.

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 = "/",
): string

Rules 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
Usage in a server loader
import { safeInternalRedirectPath } from "$lib/server/safe-redirect";
const next = url.searchParams.get("next");
const target = safeInternalRedirectPath(url, next, "/feed");
redirect(303, target);

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"

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).

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, wallet

Always import from the module barrel, never from internal files:

import { LoginForm, signInSchema } from "$lib/modules/auth";
  1. Barrel files — every module and subfolder must have an index.ts re-exporting its public API.
  2. Schemas — one Zod schema per file under schemas/.
  3. Components — feature-scoped Svelte components. Shared UI primitives stay in $lib/components/ui/.
  4. Stores — Svelte 5 rune state in .svelte.ts files. Only create when client-side state is needed.
  5. Server code stays in src/routes/ — modules contain only client-importable code. Form actions, API endpoints, and server loaders do not belong in modules.