Hydra is now in beta|Get started free|Follow our journey on X.com

SvelteKit
On this page

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 key
  • PUBLIC_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