SvelteKit + Hydra
Build a storefront using SvelteKit and the Hydra SDK. SvelteKit provides server-side rendering with +page.server.ts load functions and Svelte 5 runes for reactive client-side state.
Prerequisites
- Node.js 18+
- A Hydra project with API keys (get them from your admin panel)
1. Create your project
npx sv create my-store
cd my-store
npm install @gethydra/sdk
2. Environment variables
Create a .env file in your project root:
# Server-side only — used in +page.server.ts and API routes
HYDRA_SECRET_KEY=sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Client-side — prefixed with PUBLIC_ so SvelteKit exposes it to the browser
PUBLIC_HYDRA_PUBLISHABLE_KEY=pk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Secret keys (sk_live_*) are only available in server load functions and API routes. Publishable keys (pk_live_*) are safe for client-side code — they only allow read access and cart operations.
3. SDK setup
Create a server-side SDK instance for load functions and form actions.
// src/lib/server/hydra.ts
import { Hydra } from '@gethydra/sdk';
import { HYDRA_SECRET_KEY } from '$env/static/private';
export const hydra = new Hydra({
secret_key: HYDRA_SECRET_KEY,
base_url: 'https://api.hydrajs.dev',
});
Create a client-side instance for cart operations.
// src/lib/hydra-client.ts
import { Hydra } from '@gethydra/sdk';
import { PUBLIC_HYDRA_PUBLISHABLE_KEY } from '$env/static/public';
export const hydraClient = new Hydra({
publishable_key: PUBLIC_HYDRA_PUBLISHABLE_KEY,
base_url: 'https://api.hydrajs.dev',
});
4. Product listing page
Fetch products on the server in a +page.server.ts load function. The data is passed to the Svelte component as a prop.
// src/routes/products/+page.server.ts
import type { PageServerLoad } from './$types';
import { hydra } from '$lib/server/hydra';
export const load: PageServerLoad = async () => {
const { data: products, pagination } = await hydra.products.list({
limit: 20,
expand: 'variants,images',
});
return { products, pagination };
};
<!-- src/routes/products/+page.svelte -->
<script lang="ts">
interface Props {
data: import('./$types').PageData;
}
let { data }: Props = $props();
</script>
<h1>Products</h1>
<div class="product-grid">
{#each data.products as product}
<a href="/products/{product.handle}" class="product-card">
{#if product.images[0]}
<img
src={product.images[0].src}
alt={product.images[0].alt ?? product.title}
width={product.images[0].width}
height={product.images[0].height}
loading="lazy"
/>
{/if}
<h2>{product.title}</h2>
<p>${(product.variants[0].price / 100).toFixed(2)}</p>
</a>
{/each}
</div>
<style>
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
}
.product-card {
text-decoration: none;
color: inherit;
}
.product-card img {
width: 100%;
height: auto;
border-radius: 8px;
}
</style>
5. Product detail page
// src/routes/products/[handle]/+page.server.ts
import type { PageServerLoad } from './$types';
import { hydra } from '$lib/server/hydra';
import { error } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ params }) => {
try {
const { data: product } = await hydra.products.getByHandle(params.handle, {
expand: 'variants,images',
});
return { product };
} catch {
error(404, 'Product not found');
}
};
<!-- src/routes/products/[handle]/+page.svelte -->
<script lang="ts">
import { hydraClient } from '$lib/hydra-client';
interface Props {
data: import('./$types').PageData;
}
let { data }: Props = $props();
let adding = $state(false);
function getCartId(): string | null {
return localStorage.getItem('hydra_cart_id');
}
function setCartId(id: string) {
localStorage.setItem('hydra_cart_id', id);
}
async function addToCart(variantId: string) {
adding = true;
try {
let cartId = getCartId();
if (!cartId) {
const { data: cart } = await hydraClient.cart.create({
items: [{ variant_id: variantId, quantity: 1 }],
});
setCartId(cart.id);
} else {
await hydraClient.cart.addItem(cartId, {
variant_id: variantId,
quantity: 1,
});
}
} catch (err) {
console.error('Failed to add to cart:', err);
} finally {
adding = false;
}
}
</script>
<h1>{data.product.title}</h1>
{#if data.product.images[0]}
<img
src={data.product.images[0].src}
alt={data.product.images[0].alt ?? data.product.title}
width={data.product.images[0].width}
height={data.product.images[0].height}
/>
{/if}
{#if data.product.body_html}
{@html data.product.body_html}
{/if}
<div class="variants">
{#each data.product.variants as variant}
<div class="variant-row">
<span>{variant.title}</span>
<span>${(variant.price / 100).toFixed(2)}</span>
<button onclick={() => addToCart(variant.id)} disabled={adding}>
{adding ? 'Adding...' : 'Add to cart'}
</button>
</div>
{/each}
</div>
<style>
.variants {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-top: 1.5rem;
}
.variant-row {
display: flex;
align-items: center;
gap: 1rem;
}
</style>
6. Cart page with Svelte 5 runes
Use $state for reactive cart data and $derived for computed totals.
<!-- src/routes/cart/+page.svelte -->
<script lang="ts">
import { hydraClient } from '$lib/hydra-client';
import { onMount } from 'svelte';
interface CartItem {
id: string;
variant_id: string;
title: string;
quantity: number;
price: number;
}
interface Cart {
id: string;
items: CartItem[];
total: number;
}
let cart = $state<Cart | null>(null);
let loading = $state(true);
let checkingOut = $state(false);
let isEmpty = $derived(!cart || cart.items.length === 0);
let formattedTotal = $derived(
cart ? `$${(cart.total / 100).toFixed(2)}` : '$0.00'
);
onMount(async () => {
const cartId = localStorage.getItem('hydra_cart_id');
if (cartId) {
try {
const response = await hydraClient.cart.get(cartId);
cart = response.data;
} catch {
localStorage.removeItem('hydra_cart_id');
}
}
loading = false;
});
async function removeItem(itemId: string) {
if (!cart) return;
await hydraClient.cart.removeItem(cart.id, itemId);
const response = await hydraClient.cart.get(cart.id);
cart = response.data;
}
async function checkout() {
if (!cart) return;
checkingOut = true;
const { data: checkoutSession } = await hydraClient.checkout.create({
cart_id: cart.id,
});
window.location.href = checkoutSession.checkout_url;
}
</script>
<h1>Cart</h1>
{#if loading}
<p>Loading cart...</p>
{:else if isEmpty}
<p>Your cart is empty.</p>
<a href="/products">Browse products</a>
{:else}
<ul>
{#each cart!.items as item}
<li>
{item.title} x {item.quantity} — ${(item.price / 100).toFixed(2)}
<button onclick={() => removeItem(item.id)}>Remove</button>
</li>
{/each}
</ul>
<p><strong>Total: {formattedTotal}</strong></p>
<button onclick={checkout} disabled={checkingOut}>
{checkingOut ? 'Redirecting...' : 'Checkout'}
</button>
{/if}
7. Deploy to Cloudflare Pages
Install the Cloudflare adapter:
npm install @sveltejs/adapter-cloudflare
Update your svelte.config.js:
import adapter from '@sveltejs/adapter-cloudflare';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
export default {
preprocess: vitePreprocess(),
kit: {
adapter: adapter(),
},
};
Deploy:
npm run build
npx wrangler pages deploy .svelte-kit/cloudflare
Set your environment variables in the Cloudflare Pages dashboard under Settings > Environment variables:
HYDRA_SECRET_KEY— your secret keyPUBLIC_HYDRA_PUBLISHABLE_KEY— your publishable key
SvelteKit on Cloudflare Pages runs as a Worker, so all server load functions execute at the edge. This gives your storefront low-latency data fetching globally.
Next steps
- SDK reference — full list of available methods
- Authentication — API key types and usage
- Pagination — cursor-based pagination for large catalogs