Loomup Docs
Guide /docs/sdk-astro

Astro SDK

Package: @loomup/astro (SDK repository path: packages/astro).

Thin Astro adapter on top of @loomup/client: integration, cookie-backed SSR client, browser islands client, and optional middleware.

Install

bash
# from a monorepo checkout
cd packages/client && npm install && npm run build
cd ../astro && npm install && npm run build

# in your Astro app
npm install ../path/to/packages/astro
# or after publish:
# npm install @loomup/astro @loomup/client

Peer dependency: Astro ≥ 4.

Integration

js
// astro.config.mjs
import { defineConfig } from "astro/config";
import node from "@astrojs/node";
import loomup from "@loomup/astro";

export default defineConfig({
  output: "server", // needed for cookie auth + dynamic data
  adapter: node({ mode: "standalone" }),
  integrations: [
    loomup({
      url: process.env.LOOMUP_URL ?? "http://127.0.0.1:3000",
    }),
  ],
});

The integration sets import.meta.env.PUBLIC_LOOMUP_URL so browser code can call createBrowserClient() without an explicit URL.

Server (SSR)

Use in .astro frontmatter, API routes, and middleware. Tokens are stored in httpOnly cookies (loomup-access, loomup-refresh by default).

astro
---
import { createServerClient } from "@loomup/astro/server";

const lb = createServerClient(Astro.cookies, {
  url: import.meta.env.LOOMUP_URL ?? import.meta.env.PUBLIC_LOOMUP_URL,
});

// Optional: sign in (writes cookies)
// await lb.auth.signIn({ email, password });

const { data: todos } = await lb.from("todos").select({
  limit: 20,
  sort: "-id",
});

// Object storage (server [storage] enabled):
// await lb.storage.from("avatars").upload("u/a.png", bytes, { contentType: "image/png" });
// or multipart from an API route:
// import { uploadFromFormData, storageDownloadResponse } from "@loomup/astro/server";
// await uploadFromFormData(lb, "avatars", await Astro.request.formData(), { pathPrefix: "u/" });
---
<ul>
  {todos.map((t) => <li>{t.title}</li>)}
</ul>
MethodCookies
auth.signIn / signUpSet access + refresh
auth.refresh (and automatic 401 refresh)Rotate both
auth.signOutClear both

Cookie options:

ts
createServerClient(Astro.cookies, {
  url: "...",
  cookies: {
    names: { access: "loomup-access", refresh: "loomup-refresh" },
    secure: true, // default true when NODE_ENV=production
    path: "/",
    accessMaxAge: 3600,
    refreshMaxAge: 60 * 60 * 24 * 30,
  },
});

Realtime on the server

Do not open WebSocket subscriptions during a request-scoped SSR render. Use REST (select / get / insert / …) on the server; use islands for realtime.

Browser (islands)

ts
// src/components/TodoLive.ts  (loaded with client:load)
import { createBrowserClient } from "@loomup/astro/client";

const lb = createBrowserClient(); // uses PUBLIC_LOOMUP_URL

const list = document.querySelector("#todos");
const unsub = lb.from("todos").subscribe((ev) => {
  console.log(ev.op, ev.data);
});

// cleanup when navigating away if needed:
// unsub(); lb.closeRealtime();
astro
---
// page
---
<ul id="todos"></ul>
<script>
  import { createBrowserClient } from "@loomup/astro/client";
  const lb = createBrowserClient();
  lb.from("todos").subscribe((ev) => console.log(ev));
</script>

Authenticated islands

httpOnly cookies are not readable from JavaScript. For authenticated realtime in the browser:

  1. Pass a short-lived access token from the server into the island as a prop (only if you accept it in the HTML payload), or
  2. Sign in on the client with createBrowserClient() and keep tokens in memory / localStorage for that session.

SSR data loads stay authenticated via cookies without exposing tokens to the page.

Middleware (optional)

ts
// src/middleware.ts
import { defineMiddleware } from "astro:middleware";
import { createLoomupMiddleware } from "@loomup/astro/middleware";

const loomupMw = createLoomupMiddleware({
  url: import.meta.env.LOOMUP_URL,
  loadUser: true, // sets locals.user via auth.me()
});

export const onRequest = defineMiddleware((context, next) =>
  loomupMw(context, next),
);

Then in pages:

astro
---
const user = Astro.locals.user;
const lb = Astro.locals.loomup;
---

Add ambient types if you want TypeScript on locals:

ts
// src/env.d.ts
declare namespace App {
  interface Locals {
    user?: import("@loomup/client").User;
    loomup?: import("@loomup/astro/server").ServerLoomupClient;
  }
}

Package exports

ImportPurpose
@loomup/astroDefault loomup() integration
@loomup/astro/servercreateServerClient, cookie helpers
@loomup/astro/clientcreateBrowserClient
@loomup/astro/middlewarecreateLoomupMiddleware

Develop / test this package

bash
cd packages/astro
npm install
npm test   # tsc + node:test

See also