Skip to main content

Astro SDK

Toggly's Astro SDK provides comprehensive feature flag management for Astro applications with support for SSR, SSG, client-side hydration, and framework-specific components.

Grab the printable Astro cheat sheet (download PDF) — server .astro components, island hydration, page gating.

Overview​

The Toggly Astro SDK offers:

  • Native Astro Components - Server-rendered .astro components for optimal performance
  • Island Architecture - Client-side hydration support
  • Framework Integration - React, Vue, and Svelte component wrappers for Astro islands
  • Page-Level Gating - Control page visibility via frontmatter
  • SSR & SSG Support - Works with both rendering modes
  • User Targeting - Identity-based feature rollouts
  • Edge Ready - Optional Cloudflare Worker integration for true enforcement

Installation​

Install the Astro SDK using NPM:

npm install @ops-ai/astro-feature-flags-toggly

Supported hosts​

@ops-ai/astro-feature-flags-toggly supports Astro 5, 6, and 7 in one package (peerDependencies.astro: ^5 || ^6 || ^7). Use the Node runtime required by your Astro major and official adapters; the SDK does not raise an extra Node floor above Astro 5's requirements.

AstroExample adapters (Node / React / Vue / Svelte)
5.x@astrojs/node 9.x, @astrojs/react 4.x, @astrojs/vue 5.x, @astrojs/svelte 7.x
6.x@astrojs/node 10.x, @astrojs/react 5.x, @astrojs/vue 6.x, @astrojs/svelte 8.x
7.x@astrojs/node 11.x, @astrojs/react 6.x, @astrojs/vue 7.x, @astrojs/svelte 9.x

Optional React islands accept @nanostores/react 1 or 2. Combined React, Vue, and Svelte islands are supported in production. Current Astro 7 development cannot mix React and Vue islands because of an upstream vite-plugin-vue Fast Refresh issue; run a single island framework in astro dev until that is fixed.

Configuration​

1. Add the Integration​

Configure the Toggly integration in your astro.config.mjs:

import { defineConfig } from 'astro/config';
import togglyIntegration from '@ops-ai/astro-feature-flags-toggly/integration';

export default defineConfig({
integrations: [
togglyIntegration({
appKey: process.env.TOGGLY_APP_KEY,
environment: process.env.TOGGLY_ENVIRONMENT || 'Production',
baseURI: 'https://definitions.toggly.io',
flagDefaults: {
'example-feature': false,
},
featureFlagsRefreshInterval: 180000, // 3 minutes
isDebug: process.env.NODE_ENV === 'development',
onError: (message, error) => {
// Report build/runtime fetch and cache failures
console.warn('Toggly error:', message, error);
},
}),
],
});

2. Set Up Middleware​

Create or update src/middleware.ts to inject the Toggly client:

import { sequence } from 'astro:middleware';
import { createTogglyMiddleware } from '@ops-ai/astro-feature-flags-toggly';

const toggly = createTogglyMiddleware({
appKey: import.meta.env.TOGGLY_APP_KEY,
environment: import.meta.env.TOGGLY_ENVIRONMENT || 'Production',
});

export const onRequest = sequence(toggly);

Error reporting and fallback behavior​

Pass onError to the integration or middleware config to observe fetch, cache, parse, and refresh failures:

togglyIntegration({
appKey: process.env.TOGGLY_APP_KEY,
environment: 'Production',
onError: (message, error) => {
monitoring.captureException(error, {
tags: { source: 'toggly' },
extra: { message }
});
},
});

Astro's client store exposes error state for hydrated islands. After one successful load, refresh failures preserve the last-known-good flags instead of replacing rendered content with defaults or empty flags. React, Vue, and Svelte island helpers subscribe to flag refreshes, so client-side content can update after interval refreshes.

For the shared reliability contract, see Reliability and Error Handling.

3. Environment Variables​

Create a .env file with your Toggly credentials:

TOGGLY_APP_KEY=your-app-key
TOGGLY_ENVIRONMENT=Production
tip

Get your App Key from the Toggly application settings. Make sure to enable the Client Side API and create a Frontend-type key.

Native Astro Components​

Feature Component (Server-Side)​

The Feature.astro component evaluates feature flags on the server during SSR/SSG. This is the recommended approach for most use cases.

Basic Usage​

---
import Feature from '@ops-ai/astro-feature-flags-toggly/components/Feature.astro';
---

<Feature flag="new-dashboard">
<h1>New Dashboard</h1>
<p>This content is only visible when the feature is enabled</p>
</Feature>

Multiple Flags​

Check multiple flags with all or any requirements:

<!-- All flags must be enabled -->
<Feature flags={['feature1', 'feature2']}>
<p>Both features are enabled</p>
</Feature>

<!-- At least one flag must be enabled -->
<Feature flags={['feature1', 'feature2']} requirement="any">
<p>At least one feature is enabled</p>
</Feature>

On and off paths​

Use a second <Feature negate> for the off path:

<Feature flag="premium-feature">
<div class="premium-content">
<h2>Premium Features</h2>
<p>Access exclusive content</p>
</div>
</Feature>
<Feature flag="premium-feature" negate={true}>
<h2>Upgrade to Premium</h2>
<p>Unlock exclusive features</p>
</Feature>

Negation only​

Show content when a feature is disabled:

<Feature flag="old-feature" negate={true}>
<p>This shows when the feature is OFF</p>
</Feature>

FeatureClient Component (Client-Side)​

Use FeatureClient.astro for client-side evaluation with Astro islands. This component hydrates on the client and can respond to flag changes without page reloads.

Hydration Strategies​

---
import FeatureClient from '@ops-ai/astro-feature-flags-toggly/components/FeatureClient.astro';
---

<!-- Hydrate immediately on page load -->
<FeatureClient flag="interactive-widget" client="load">
<InteractiveWidget />
</FeatureClient>

<!-- Hydrate when visible in viewport -->
<FeatureClient flag="below-fold-content" client="visible">
<HeavyComponent />
</FeatureClient>

<!-- Hydrate when browser is idle -->
<FeatureClient flag="non-critical-feature" client="idle">
<NonCriticalContent />
</FeatureClient>

When to Use Client Components​

  • Dynamic UI that updates without page reload
  • Non-critical features that can load after initial render
  • Below-the-fold content that should lazy-load
  • Features that depend on user interaction

Page-Level Gating​

Control entire pages using frontmatter. This approach is more efficient than wrapping all page content in a component.

---
// src/pages/beta-feature.astro
x-feature: beta-feature
title: Beta Feature Page
---

<html>
<head>
<title>{frontmatter.title}</title>
</head>
<body>
<h1>Beta Feature Page</h1>
<p>This entire page is gated by the 'beta-feature' flag</p>
</body>
</html>

The integration automatically:

  • Extracts x-feature from page frontmatter during build
  • Generates a toggly-page-features.json manifest
  • Maps routes to feature flags

For true edge enforcement (404 responses for disabled pages), deploy the Toggly Cloudflare Worker in front of your origin. It reads the manifest, fetches GET https://definitions.toggly.io/evaluated-signed/{appKey}/{environment}, returns 404 (or a redirect) for disabled pages, and strips [data-feature] from HTML.

Framework Integration​

React in Astro Islands​

Use Toggly in React components within Astro:

// src/components/Dashboard.tsx
import { Feature, useFeatureFlag } from '@ops-ai/astro-feature-flags-toggly/react';

// Component-based — on and off with negate
export function Dashboard() {
return (
<>
<Feature flag="new-dashboard">
<NewDashboard />
</Feature>
<Feature flag="new-dashboard" negate>
<OldDashboard />
</Feature>
</>
);
}

// Hook-based (includes local post-filter gates via $flag / $gate)
export function ConditionalContent() {
const { enabled, isReady } = useFeatureFlag('premium-feature');

if (!isReady) return <Loading />;
if (!enabled) return <FreeTier />;
return <PremiumTier />;
}

// Render prop for styling/behavior while content stays mounted
export function CheckoutButton() {
return (
<Feature
flag="PremiumCheckout"
render={(enabled) => (
<button className={enabled ? 'active' : ''} disabled={!enabled}>
Checkout
</button>
)}
/>
);
}

Island <Feature>, useFeatureFlag, useFeatureGate, and FeatureClient.astro evaluate through $gate / $flag, so device-local post-filter gates apply without extra wiring.

In your Astro page:

---
import Dashboard from '../components/Dashboard.tsx';
---

<Dashboard client:load />

Vue in Astro Islands​

<!-- src/components/Features.vue -->
<script setup>
import Feature from '@ops-ai/astro-feature-flags-toggly/vue/Feature.vue';
import { useFeatureFlag } from '@ops-ai/astro-feature-flags-toggly/vue';
import FeatureGateBuilder from '@ops-ai/astro-feature-flags-toggly/vue/FeatureGateBuilder.vue';

const { enabled } = useFeatureFlag('new-feature');
</script>

<template>
<Feature flag="beta-widget">
<BetaWidget />
</Feature>
<Feature flag="beta-widget" :negate="true">
<ComingSoon />
</Feature>

<!-- Conditional UI (styling, taps) — content stays mounted -->
<FeatureGateBuilder flag="PremiumCheckout" v-slot="{ enabled }">
<button :class="{ active: enabled }" :disabled="!enabled">
Sales
</button>
</FeatureGateBuilder>

<div v-if="enabled">
<p>Feature-controlled content</p>
</div>
</template>

Svelte in Astro Islands​

<!-- src/components/Widget.svelte -->
<script>
import Feature from '@ops-ai/astro-feature-flags-toggly/svelte/Feature.svelte';
import { featureFlag } from '@ops-ai/astro-feature-flags-toggly/svelte';
import FeatureGateBuilder from '@ops-ai/astro-feature-flags-toggly/svelte/FeatureGateBuilder.svelte';

const newDashboard = featureFlag('new-dashboard');
</script>

<Feature flag="beta-feature">
<BetaContent />
</Feature>
<Feature flag="beta-feature" negate={true}>
<RegularContent />
</Feature>

<FeatureGateBuilder flag="PremiumCheckout" let:enabled>
<button class:active={enabled} disabled={!enabled}>Sales</button>
</FeatureGateBuilder>

{#if $newDashboard}
<NewDashboard />
{:else}
<OldDashboard />
{/if}

SSR vs SSG Considerations​

Server-Side Rendering (SSR)​

With output: 'server' in your Astro config:

  • Flags are fetched on each request
  • Fresh flag values for every visitor
  • Slightly slower initial page load
  • Best for dynamic content that needs latest flags
// astro.config.mjs
export default defineConfig({
output: 'server',
adapter: node(),
// ...
});

Static Site Generation (SSG)​

With output: 'static' in your Astro config:

  • Flags are fetched at build time
  • Same flag values for all visitors until next build
  • Fastest possible page loads
  • Requires rebuild to update flags
  • Use client components for dynamic updates
// astro.config.mjs
export default defineConfig({
output: 'static',
// ...
});

Hybrid Strategy​

Combine server and client components:

---
import Feature from '@ops-ai/astro-feature-flags-toggly/components/Feature.astro';
import FeatureClient from '@ops-ai/astro-feature-flags-toggly/components/FeatureClient.astro';
---

<!-- Critical: evaluated at build/request time -->
<Feature flag="access-control">
<SecureContent />
</Feature>

<!-- Non-critical: evaluated on client -->
<FeatureClient flag="ui-enhancement" client="idle">
<EnhancedUI />
</FeatureClient>

Advanced Features​

User Identity for Targeting​

Set user identity for accurate targeting and rollouts:

// In middleware, layout, or component
import { setIdentity } from '@ops-ai/astro-feature-flags-toggly';

// After user login
setIdentity('user-123');

// On logout
import { clearIdentity } from '@ops-ai/astro-feature-flags-toggly';
clearIdentity();

Configure identity, groups, and claims on the integration (init-time for groups/claims — runtime setIdentity updates identity only):

togglyIntegration({
appKey: process.env.TOGGLY_APP_KEY,
environment: 'Production',
identity: getUserId(), // or a string user id
groups: ['beta', 'enterprise'],
claims: { role: 'admin', plan: 'premium' },
enableLiveUpdates: true, // default — island WebSocket sync
verifySignatures: true, // optional ES256 envelope verification
})

Init groups / claims are forwarded into local EvalContext (including claims for UserClaims filters). The Astro server rail does not wire per-request EvalContext.request (UA / Accept-Language / country), so HTTP segment filters fail closed on that path unless you evaluate elsewhere. See SDK × filter matrix.

See Feature filters, Evaluated-signed, and WebSocket sync.

success

When using user identifiers, evaluated features are cached per user for 30 minutes by default on a sliding window, ensuring consistent feature visibility during a session.

Manual Flag Refresh​

Manually refresh flags from the API:

import { refreshFlags } from '@ops-ai/astro-feature-flags-toggly';

// Refresh flags
await refreshFlags();

Programmatic Evaluation​

Access flags programmatically in server-side code:

---
const toggly = Astro.locals.toggly;

// Get single flag
const isEnabled = await toggly.getFlag('feature-key');

// Get all flags
const allFlags = await toggly.getFlags();

// Evaluate complex gates
const hasAccess = await toggly.evaluateGate(
['feature1', 'feature2'],
'any',
false
);
---

{isEnabled && <FeatureContent />}

Edge Enforcement​

For true enforcement at the edge (prevents access even with JavaScript disabled):

  1. The integration generates toggly-page-features.json during build
  2. Deploy the Toggly Cloudflare Worker so it reads that manifest
  3. The worker returns 404 or redirects for pages with disabled flags, and strips [data-feature] from HTML

Configuration Options​

interface TogglyConfig {
// Base URI for the Toggly API
baseURI?: string; // default: 'https://definitions.toggly.io'

// Your application key from Toggly
appKey?: string;

// Environment name
environment?: string; // default: 'Production'

// Default values when API is unavailable
flagDefaults?: Record<string, boolean>;

// Refresh interval in milliseconds
featureFlagsRefreshInterval?: number; // default: 180000 (3 min)

// WebSocket live updates for hydrated islands (default: true)
enableLiveUpdates?: boolean;

// Verify ES256 signed envelopes via JWKS (default: false)
verifySignatures?: boolean;
allowedKeyIds?: string[];
maxSignatureAgeSeconds?: number;

// Enable debug logging
isDebug?: boolean; // default: false

// API timeout in milliseconds
connectTimeout?: number; // default: 5000

// User identity for targeting
identity?: string;

// Init-time groups and claims for targeting
groups?: string[];
claims?: Record<string, string>;
}

TypeScript Support​

The SDK includes full TypeScript support with type definitions:

import type {
TogglyConfig,
Flags,
TogglyClient,
FeatureProps,
} from '@ops-ai/astro-feature-flags-toggly';

// Astro global is augmented
const toggly = Astro.locals.toggly; // Typed as TogglyClient

Entity context​

Pass the page entity on each <Feature> or server getFlag call — not on user identity. See Entity & page context.

Use a canonical entity object in native components and browser islands. This does not register schemas with the dashboard. Keep user targeting separate from these per-check attributes.

// Canonical context travels with this one entity evaluation.
const orderContext = {
kind: 'Order',
key: String(order.id),
attributes: { Status: order.status },
}
---
const order = Astro.props.order
const orderContext = { kind: 'Order', key: String(order.id),
attributes: { Status: order.status } }
---
<Feature flag="OrderBadge" context={orderContext}>
<span class="badge">Featured</span>
</Feature>

Gates in mixed definitions resolve per island / per check. Missing context fails closed. Keep identity for user rollouts only.

Best Practices​

  1. Prefer Server Components - Use Feature.astro when possible for better performance
  2. Set Flag Defaults - Always provide fallback values for offline scenarios
  3. Use Environment Variables - Store credentials securely, never hardcode
  4. Enable Debug in Development - Use isDebug: true to troubleshoot issues
  5. Choose SSR vs SSG Wisely - Consider how dynamic your flags need to be
  6. Use Page-Level Gating - More efficient than wrapping entire page content
  7. Provide User Identity - Required for targeting and consistent rollouts
  8. Cache Appropriately - Adjust featureFlagsRefreshInterval based on needs

Complete Example​

Here's a complete example combining multiple patterns:

---
// src/pages/index.astro
import Feature from '@ops-ai/astro-feature-flags-toggly/components/Feature.astro';
import FeatureClient from '@ops-ai/astro-feature-flags-toggly/components/FeatureClient.astro';
import Layout from '../layouts/Layout.astro';

const toggly = Astro.locals.toggly;
const showBanner = await toggly.getFlag('promotional-banner');
---

<Layout title="Home">
<!-- Server-rendered promotional banner -->
{showBanner && (
<div class="banner">
<p>Special offer - 50% off!</p>
</div>
)}

<!-- Critical feature - server-rendered on / off -->
<Feature flag="new-homepage-design">
<NewHomepage />
</Feature>
<Feature flag="new-homepage-design" negate={true}>
<OldHomepage />
</Feature>

<!-- Non-critical widget - client-side -->
<FeatureClient flag="social-widgets" client="visible">
<SocialWidgets />
</FeatureClient>

<!-- Multiple flags -->
<Feature flags={['feature-a', 'feature-b']} requirement="any">
<EnhancedExperience />
</Feature>
</Layout>

Troubleshooting​

Flags Not Loading​

  1. Verify integration is configured in astro.config.mjs
  2. Check appKey and environment values
  3. Enable isDebug: true to see logs
  4. Check browser console and server logs
  5. Verify Client Side API is enabled in Toggly settings

TypeScript Errors​

Ensure dependencies are installed:

npm install -D @types/node astro typescript

Client Components Not Hydrating​

  • Verify you're using a hydration directive: client:load, client:idle, or client:visible
  • Check that the component is properly imported
  • Look for JavaScript errors in browser console

Middleware Not Working​

  • Ensure src/middleware.ts exports onRequest
  • Verify middleware is properly configured with Toggly
  • Check that the integration is loaded before middleware runs

Extensibility with Hooks​

Toggly provides a powerful hooks system that allows you to extend SDK functionality by hooking into feature flag lifecycle events. This is perfect for integrating with analytics platforms like Microsoft Clarity, monitoring tools, or implementing custom behaviors.

What are Hooks?​

Hooks let you execute custom code at specific points in the feature flag evaluation lifecycle:

  • beforeEvaluation: Called before a feature flag is evaluated
  • afterEvaluation: Called after a feature flag is evaluated (with the result)
  • beforeIdentify: Called before user identity is set or cleared
  • afterIdentify: Called after user identity is set or cleared
  • afterRefresh: Called after feature definitions are refreshed from Toggly

Creating a Hook​

import type { Hook } from '@ops-ai/toggly-hooks-types';

const myAnalyticsHook: Hook = {
getMetadata: () => ({
name: 'MyAnalyticsHook',
version: '1.0.0'
}),

afterEvaluation: async (flagKey, _data, result) => {
// Send to analytics
if (typeof analytics !== 'undefined') {
analytics.track('Feature Flag Evaluated', {
feature: flagKey,
enabled: result
});
}
}
};

Registering Hooks​

During initialization in astro.config:​

// astro.config.mjs
import { defineConfig } from 'astro/config';
import toggly from '@ops-ai/astro-feature-flags-toggly/integration';

export default defineConfig({
integrations: [
toggly({
appKey: 'your-app-key',
environment: 'your-environment-name',
hooks: [myAnalyticsHook]
})
]
});

At runtime (client-side):​

After the client is initialized, add and remove hooks through the client store API:

import { addHook, removeHook } from '@ops-ai/astro-feature-flags-toggly/client/store';

addHook(myAnalyticsHook);
// Remove by the stable name returned by getMetadata(), not the hook object.
removeHook(myAnalyticsHook.getMetadata().name);

In an Astro <script>, use the same imports. Remove the hook when the owning client component is disposed so mounting it again does not duplicate callbacks.

Common Use Cases​

Microsoft Clarity Integration​

import type { Hook } from '@ops-ai/toggly-hooks-types';

const clarityHook: Hook = {
getMetadata: () => ({ name: 'Microsoft Clarity', version: '1.0.0' }),
afterEvaluation: async (flagKey, _data, result) => {
if (typeof clarity !== 'undefined') {
clarity('event', `FeatureFlag:${flagKey}`);
}
}
};

Google Analytics 4 Integration​

import type { Hook } from '@ops-ai/toggly-hooks-types';

const ga4Hook: Hook = {
getMetadata: () => ({ name: 'Google Analytics 4', version: '1.0.0' }),
afterEvaluation: async (flagKey, _data, result) => {
if (typeof gtag !== 'undefined') {
gtag('event', 'feature_flag_evaluated', {
feature_name: flagKey,
feature_enabled: result
});
}
}
};

Debug Logging (Development Only)​

import type { Hook } from '@ops-ai/toggly-hooks-types';

const debugHook: Hook = {
getMetadata: () => ({ name: 'DebugLogger', version: '1.0.0' }),
afterEvaluation: async (flagKey, _data, result) => {
if (import.meta.env.DEV) {
console.debug('[Toggly]', flagKey, '=', result);
}
}
};

See also​

Sample references​

Browse the Toggly Samples catalog for hands-on examples. Start with a sample’s README for setup instructions, then follow its source walkthrough to see how configuration, flag checks, and UI behavior fit together.

Next Steps​