Feature Flags
Feature flags let you ship code dark, roll it out to a percentage of users, or flip it on for a single account, all without a redeploy. Flags are evaluated on the server and the resolved assignment streams to the client over the same SSP + sync pipeline that powers useQuery, so toggling a flag from your terminal updates the running UI without a refresh.
The important property: targeting rules never reach the client. Allowlists, rollout percentages, and flag definitions live in a root-only table. The browser only ever sees the single variant resolved for the signed-in user.
How it works
Two server tables back the whole system:
| Table | Who can read it | Who can write it |
|---|---|---|
_00_feature_flag | nobody (root only) | spky flag CLI + scheduler |
_00_user_feature | each user, their own rows only | nobody but root |
- You write a flag definition (variants, default, targeting rules) into
_00_feature_flagwith thespky flagCLI. - On every write the CLI runs the evaluator inline and materializes one row per existing user into
_00_user_featurewith their resolved variant. - The scheduler runs a periodic sweep that fills in assignments for users who signed up since the last write, so new accounts get their flags within seconds.
- The client subscribes to its own
_00_user_featurerow. When the materialized variant changes, the new value rides the existing websocket straight into your reactive UI.
Because writes to _00_user_feature are root-only and a user can only ever
SELECT their own row, a malicious client cannot self-enable a flag or
discover who else is in a rollout. Permissions are enforced by SurrealDB,
not by the client.
1. Define a flag
A flag has a set of variants, a default, and an optional description. With no variants specified you get a boolean flag (off, on) defaulting to off.
2. Target users
A fresh flag resolves to its default for everyone. Add targeting rules to change that. There are two kinds:
| Rule | Set with | Priority |
|---|---|---|
| Allowlist | --for-user <username> | wins |
| Rollout | --rollout <0-100> | evaluated after allowlists |
An allowlisted user always gets their variant, even if a rollout would have excluded them. Rollout bucketing is a stable hash of (flag key, user id), so the same user stays in (or out of) the rollout across deploys, and raising the percentage only ever adds users.
spky flag disable is a global kill switch: it forces every user back to the
flag’s default variant regardless of any rules, without deleting them.
spky flag enable restores rule evaluation.
3. Read it in your app
SolidJS
useFeatureFlag returns three reactive accessors that update live whenever the server assignment changes.
| Prop | Type | Default | Description |
|---|---|---|---|
| key | string | - | The flag key, as created with `spky flag create`. |
| options.fallback | string | undefined | Variant returned before the server assignment arrives, or when the user has none. |
| options.ttl | QueryTimeToLive | '10m' | How long the underlying live query stays registered without activity. |
The returned object exposes:
| Accessor | Type | Description |
|---|---|---|
variant() | string | undefined | The variant resolved for the current user (or the fallback). |
enabled() | boolean | true when the variant is set and not 'off'. Use for boolean flags. |
payload() | unknown | undefined | The JSON payload attached to the resolved variant, if any. |
Multi-variant flags
For experiments with more than two variants, read variant() directly instead of enabled(). Each variant can carry an arbitrary JSON payload (configured on the flag definition’s payloads map) that you read with payload().
Vanilla JS / TS
Outside of SolidJS, call feature() on your Sp00kyClient directly. It returns a FeatureFlagHandle with the same variant() / enabled() / payload() methods plus a subscribe() for change notifications.
CLI reference
Scheduler sweep
The scheduler sweep that backfills assignments for new signups runs every 30 seconds by default. Tune it with the SPKY_FEATURE_FLAG_SWEEP_SECS environment variable (or feature_flag_sweep_interval_secs in sp00ky.yml). Existing assignments are never touched by the sweep, the CLI keeps those current on every write, so a quiet table costs one cheap query per flag per tick.