Skip to content

Astro

Astro pages render on the server; the gates client runs in the browser. The pattern: one shared client module, and the React components inside islands.

Setup

sh
pnpm astro add react
pnpm add @lucerna-dev/gates-browser @lucerna-dev/identity

Using Preact instead of React? The Preact aliases work the same inside Astro's Vite config.

Create the shared client module:

ts
// src/lib/gates.ts
import { createGates } from "@lucerna-dev/gates-browser";
import { createIdentity } from "@lucerna-dev/identity";

export const identity = createIdentity({ apiKey: "ck_client_prod_…" });
export const gates = createGates({ clientKey: "ck_client_prod_…", identity });

This module is browser state — import it from island components, never from .astro frontmatter (frontmatter runs on the server, where there is no user).

Islands

Context doesn't cross islands

Each island is its own React tree, so each one mounts its own provider. Wrap once per island with a shared helper — every island still answers from the same client and the same single bootstrap.

tsx
// src/components/GatesIsland.tsx
import { GatesProvider } from "@lucerna-dev/gates-browser/react";
import type { ReactNode } from "react";
import { gates } from "../lib/gates";

export function GatesIsland({ children }: { children: ReactNode }) {
  return <GatesProvider client={gates}>{children}</GatesProvider>;
}
tsx
// src/components/Billing.tsx
import { Feature } from "@lucerna-dev/gates-browser/react";
import { GatesIsland } from "./GatesIsland";

export function Billing() {
  return (
    <GatesIsland>
      <Feature name="new_billing" fallback={<a href="/billing">Billing</a>}>
        <a href="/billing/v2">Billing</a>
      </Feature>
    </GatesIsland>
  );
}
astro
---
// src/pages/index.astro
import { Billing } from "../components/Billing";
---

<Billing client:load />

Rendering semantics

  • With client:load (or client:idle / client:visible), Astro server-renders the island first. On the server decisions don't exist yet, so the HTML shows the fail-open branch (fallback, flags off); the island corrects itself once the bootstrap answers.
  • If that first-paint flicker matters, pass storage to createGates so returning visitors hydrate straight into real decisions — or evaluate server-side and pass the result down as props.
  • client:only="react" skips the server render entirely — the island appears only after hydration.

Static output

Decisions are fetched at runtime in the visitor's browser, so everything above works identically with output: "static" — flag changes don't require a rebuild.

Lucerna Developer Docs