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
.astrocomponents 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.
| Astro | Example 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
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-featurefrom page frontmatter during build - Generates a
toggly-page-features.jsonmanifest - 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.
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):
- The integration generates
toggly-page-features.jsonduring build - Deploy the Toggly Cloudflare Worker so it reads that manifest
- 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
- Prefer Server Components - Use
Feature.astrowhen possible for better performance - Set Flag Defaults - Always provide fallback values for offline scenarios
- Use Environment Variables - Store credentials securely, never hardcode
- Enable Debug in Development - Use
isDebug: trueto troubleshoot issues - Choose SSR vs SSG Wisely - Consider how dynamic your flags need to be
- Use Page-Level Gating - More efficient than wrapping entire page content
- Provide User Identity - Required for targeting and consistent rollouts
- Cache Appropriately - Adjust
featureFlagsRefreshIntervalbased 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
- Verify integration is configured in
astro.config.mjs - Check
appKeyandenvironmentvalues - Enable
isDebug: trueto see logs - Check browser console and server logs
- 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, orclient:visible - Check that the component is properly imported
- Look for JavaScript errors in browser console
Middleware Not Working
- Ensure
src/middleware.tsexportsonRequest - 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
- Learn about User Targeting and Rollouts
- Explore Metrics and Monitoring
- Check out Cloudflare Workers Integration
- Read about Other JavaScript SDKs