Back to Projects
WIPSaaS2026

Gatedly

Open-source feature flag service built in Go. Ship features confidently with gradual rollouts, percentage-based targeting, allow/deny lists, and custom condition rules. Designed for teams who want LaunchDarkly-level control without the price tag.

GoReactTypeScriptPostgreSQL

Feature flags should not cost $400 a month. Gatedly is an open-source feature flag service built in Go — LaunchDarkly-level control with gradual rollouts, percentage targeting, and custom rule conditions, without the enterprise pricing.

Why Another Feature Flag Tool

LaunchDarkly is the gold standard for feature flags. It is also priced for enterprises. Flagsmith, Unleash, and the other open-source alternatives are either complex to self-host or missing features that matter in production — specifically percentage-based rollouts, allow/deny lists on the same flag, and custom condition rules evaluated server-side.

Gatedly is designed for small teams and solo developers who want to ship confidently with gradual rollouts but do not need a $400/month SaaS. It is self-hostable in a single Docker container and has a React dashboard for managing flags without touching config files.

Architecture

The backend is Go — chosen for its low memory footprint (important for self-hosting on cheap VPS instances), fast startup, and excellent standard library for HTTP APIs. PostgreSQL stores flag definitions, rules, and evaluation logs. The evaluation engine runs entirely in-process; there is no external dependency required at evaluation time.

Gatedly backend (Go)
  └── Flag CRUD API (Gin)
  └── Evaluation engine (in-process)
  └── SDK endpoint: GET /evaluate/:flagKey
  └── PostgreSQL (flags, rules, environments, logs)
  └── Server-Sent Events (real-time flag updates)

React dashboard
  └── Flag management UI
  └── Targeting rules editor
  └── Evaluation log viewer

The Evaluation Engine

Flag evaluation is the core piece. A flag is evaluated for a context — typically a user ID with optional attributes. The engine walks through the flag's rules in priority order:

type EvaluationContext struct {
  UserID     string
  Attributes map[string]any
}

// Rule priority order:
// 1. Allow list (user IDs get flag regardless of anything else)
// 2. Deny list  (user IDs never get flag)
// 3. Custom conditions (attribute matching)
// 4. Percentage rollout (consistent hash of userID + flagKey)
// 5. Default variation

Percentage rollouts use a consistent hash of the user ID combined with the flag key. This means the same user always gets the same bucket, rollouts are sticky, and changing the percentage only affects users at the boundary — not the entire user base.

func percentageBucket(userID, flagKey string) int {
  h := fnv.New32a()
  h.Write([]byte(userID + ":" + flagKey))
  return int(h.Sum32() % 100)
}

func (e *Engine) evaluatePercentage(ctx EvaluationContext, flag Flag) bool {
  bucket := percentageBucket(ctx.UserID, flag.Key)
  return bucket < flag.RolloutPercentage
}

Custom Condition Rules

Beyond percentage and lists, flags support condition rules evaluated against context attributes. Rules support string equality, contains, starts-with, numeric comparisons, and semantic version comparisons — the last one being important for mobile app rollouts where you want to target users on a specific app version range.

// Example: enable for premium users on app version >= 2.1.0
{
  "conditions": [
    { "attribute": "plan", "operator": "eq", "value": "premium" },
    { "attribute": "appVersion", "operator": "semver_gte", "value": "2.1.0" }
  ],
  "match": "all"
}

Real-Time Flag Updates

When a flag is updated in the dashboard, connected SDK clients receive the change via Server-Sent Events without polling. The Go SSE implementation maintains a registry of active subscribers per environment. Flag updates are broadcast to all subscribers in that environment within milliseconds.

The SDK

A lightweight TypeScript SDK wraps the evaluation API. It caches flag values locally and subscribes to the SSE stream for updates. The cache means flag evaluations are synchronous after the initial fetch — zero latency at the point of evaluation.

import { Gatedly } from '@gatedly/sdk';

const client = new Gatedly({ apiKey: process.env.GATEDLY_KEY });
await client.init();

// Synchronous after init — no async/await needed
const showNewCheckout = client.isEnabled('new-checkout', {
  userId: user.id,
  attributes: { plan: user.plan, country: user.country }
});

Status

Gatedly is in active development. The evaluation engine, flag CRUD API, percentage rollouts, allow/deny lists, and custom conditions are complete. The SSE real-time updates and dashboard UI are in progress. The TypeScript SDK is planned for after the core API stabilises.

Interested in working together?

I am available for new projects — whether you have a clear brief or just an idea worth exploring.