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