Skip to main content

SolidJS SDK

@ops-ai/solid-feature-flags-toggly provides native SolidJS browser feature flags. Use it with Toggly or with local defaults. A feature flag chooses which behavior is shown without a new application deployment. UI gates do not replace server authorization.

Grab the printable SolidJS / SolidStart cheat sheet (download PDF) — provider, Feature + negate, SolidStart snapshot.

Installation​

npm install @ops-ai/solid-feature-flags-toggly solid-js

Requires SolidJS 1.9+ and Node 22.12+ for tooling. Configure Vite with vite-plugin-solid; the package exports preserved JSX through the solid condition, so the consumer compiles it with the same Solid runtime. Browsers need Fetch, AbortController and WebCrypto for signed definitions. For Node-hosted SSR, use the SolidStart integration with its separate Node-only server entrypoint, signed public snapshots and reactive hydration.

Provider and declarative gates​

import { Feature, TogglyProvider } from '@ops-ai/solid-feature-flags-toggly';

export default function App() {
return (
<TogglyProvider
config={{
appKey: import.meta.env.VITE_TOGGLY_APP_KEY,
environment: 'Production',
flagDefaults: { 'new-dashboard': false },
identity: 'alice',
groups: ['staff'],
claims: { role: 'admin' },
}}
>
<Feature feature="new-dashboard" loading={<p>Loading…</p>}>
<p>New dashboard</p>
</Feature>
<Feature feature="new-dashboard" negate>
<p>Classic dashboard</p>
</Feature>
<Feature feature={['new-dashboard', 'api-v2']} requirement="all">
Both enabled
</Feature>
<Feature feature={['new-dashboard', 'api-v2']} requirement="any">
Either enabled
</Feature>
</TogglyProvider>
);
}

Configuration is read once when the provider is created. Change targeting using client.setContext; changing the config prop does not reconfigure a running provider. App keys are browser-visible identifiers; never supply a management credential. Missing keys evaluate false. Empty key lists evaluate true before negation. Each Feature block keeps its children lazy: an expensive or lazy() component is instantiated only when its branch is selected. Disabled content belongs in a separate Feature block with negate and the same gate parameters. While a request runs, both blocks suppress their children; put loading content on only one block to avoid duplicate indicators. Loading content is empty by default.

Reactive and programmatic API​

import { createSignal, Suspense } from 'solid-js';
import { useFeatureFlag, useFeatureFlags, useToggly } from '@ops-ai/solid-feature-flags-toggly';

function Dashboard() {
const [key] = createSignal('new-dashboard');
const enabled = useFeatureFlag(key); // also accepts a fixed string
const definitions = useFeatureFlags(); // raw boolean/entity-gate definitions
const toggly = useToggly();
return (
<>
<p>{enabled() ? 'Enabled' : 'Disabled'}</p>
<button onClick={() => toggly.client.refresh()}>Refresh</button>
<Suspense fallback={<p>Loading definitions…</p>}>
<pre>{JSON.stringify(toggly.resource())}</pre>
</Suspense>
<pre>{JSON.stringify(definitions())}</pre>
<p>{toggly.error()?.message}</p>
</>
);
}

useFeatureFlag returns a memoized boolean accessor, so downstream computations run when that result changes. useToggly().evaluate(keys, requirement?, negate?, entity?) is synchronous and reactive within a Solid computation. loading, error, and flags are accessors. resource integrates initial fetching with Suspense; later manual/live refreshes are observed through flags and loading. Failures expose error() and retain same-context last-known definitions or configured defaults; the resource resolves the fallback rather than throwing.

For an owner without context, call createToggly(config) within a component or createRoot. Owner disposal unsubscribes, aborts HTTP and closes timers/sockets automatically. createClient(config) is the lower-level non-reactive instance API: explicitly call refresh, start and dispose, and use subscribe to observe state. There is no global client.

Identity and targeting​

await toggly.client.setContext({
identity: 'alice',
groups: ['staff'],
claims: { role: 'admin' },
});
await toggly.client.setContext({ identity: '', groups: [], claims: {} }); // explicit clear

Omitted fields preserve the current value; empty values clear it. Initial targeting is copied, and each provider owns its session. A change clears previous identity results before fetching; older in-flight responses cannot overwrite newer results. Identity is not generated or persisted automatically. Group, percentage and claim rules are evaluated remotely. Browser country, language and device rules reflect the real network/browser request; a demo cannot impersonate these by changing claims.

Entity and local gates​

const order = { kind: 'Order', key: 'ord-vip', attributes: { Vip: true } };
const enabled = useFeatureFlag('ExpressCheckout', () => order);
<Feature feature="ExpressCheckout" entity={order}>
Express checkout
</Feature>;

let deviceReady = false;
toggly.client.setLocalGates([
{ id: 'device-ready', flagKeys: ['api-v2'], isEnabled: () => deviceReady },
]);
deviceReady = true;
toggly.client.notifyLocalGatesChanged();

Pass a complete TogglyEntityContext explicitly; this SDK does not register process-wide entity mappers. Toggly returns entity conditions with evaluated definitions and the shared evaluator resolves them against attributes. Entity gates without context fail closed. Local gates AND their value with the evaluated result, so they can disable but cannot independently enable a remote-disabled feature. Notify after non-reactive device state changes; a local gate reading a Solid signal is tracked during evaluation. A flag cannot belong to two different local gates.

Defaults and failure behavior​

<TogglyProvider config={{ flagDefaults: { 'new-dashboard': true } }}>
<Feature feature="new-dashboard">Offline preview</Feature>
</TogglyProvider>

Without an app key, the client performs no definitions requests and uses defaults. The SDK never invents a key. Applications should display a visible configuration banner when live mode is expected.

Signed definitions, cache and live updates​

Signature verification defaults to true. The shared client verifies ES256 evaluated-signed responses using the service JWKS before applying booleans or entity gates. Configure allowedKeyIds to constrain trusted keys and maxSignatureAgeSeconds to reject old envelopes. Set verifySignatures: false only for explicitly unsigned development fixtures.

Each client keeps a same-context in-memory snapshot and conditional HTTP revision. Persistent caching is opt-in: supply the application's storage adapter. The SDK stores the exact signed envelope with the public key that verified it. A fresh client rechecks the signature, current key pins, key metadata/expiry, configured signature age and complete entity schema before restoring definitions, without a network key fetch. Refresh still attempts the service; an offline error leaves the verified restored flags usable. With no matching valid record, defaults remain active.

const storage = {
getItem: (key: string) => window.localStorage.getItem(key),
setItem: (key: string, value: string) => window.localStorage.setItem(key, value),
};
// Use in browser configuration; the callbacks defer storage access until refresh.
const client = createClient({ appKey: 'your-frontend-app-key', storage });
await client.refresh();

Signed SSR snapshots and values already accepted from the network or verified storage take precedence over persisted records. Restore runs only while the current context has defaults; changing context or hydrating a source: 'defaults' snapshot makes its matching cache eligible again. A source: 'signed' snapshot remains authoritative during offline refresh.

Cache records are partitioned by endpoint, app, environment and complete targeting URL. Signing-key notifications retire all stored targeting records for that endpoint before refreshing. Corrupt/unsupported records and inaccessible storage cannot enable cached flags or prevent network recovery. If retirement cannot be written, that client stops using persistence for its lifetime; restoring storage access or clearing the affected storage is the application's responsibility before a later restart.

Storage is application/origin-owned local trust material. A party able to replace both stored keys and envelopes can replace that trust anchor unless you independently configure allowedKeyIds. Signature verification is repeated on every restore; a signed timestamp floor rejects rollback within a running context. Detecting rollback of the entire store after process loss requires external protected state. maxSignatureAgeSeconds limits how old a persisted envelope may be at restart; unset/nonpositive values disable the age limit. Future timestamps and expired keys are rejected. Storage keys include identity/groups/claims; clear or partition storage according to your application's privacy and logout policy. Caching definitions does not make application HTML, assets or server queries available offline.

Set expose to restrict browser definitions to an explicit list of public keys. A server snapshot supplies its own allowlist, which also applies to subsequent refreshes. Every fetch uses cache: 'no-store'; polling sends its explicit confirmed ETag, while revisionless invalidations omit validators.

On mount, live WebSocket updates are enabled by default and coalesced for 300 ms. Revision notifications trigger a pinned HTTP fetch; revisions are accepted only after HTTP confirmation. Signing-key changes clear JWKS. Connections retry after 5 seconds and polling remains a fallback. Use enableLiveUpdates: false to disable sockets, refreshInterval: 0 to disable polling, or connectTimeout to set request timeout (10,000 ms default). Polling defaults to 180,000 ms. baseURI defaults to https://definitions.toggly.io; environment defaults to Production. fetch can inject a transport for tests. The browser entrypoint exposes no variant assignment, usage metrics or analytics hook API; a boolean fallback is not an experiment assignment.

Development​

npm install
npm run typecheck
npm run build
npm run test:coverage
npm pack --dry-run

Tests cover real WebCrypto envelopes and tampering, cache failures, identity races, fine-grained rendering, lazy children, Suspense, live notifications and disposal. See the complete SolidJS sample for the interactive workshop and app setup.

License​

MIT. Documentation · Toggly.