Cloud Architecture·8 min read·

Config and Feature Flags Without a SaaS Service

Ship flags as Postgres rows plus a local cache so you can dark-launch and halt features without a vendor SDK or a round trip on every request.

NB

Netbay Cloud Team

Netbay Engineering

On this page

Feature flags are an operational control plane, not a marketing dashboard. You need them to halt a bad payment path at 02:00, to dark-launch a GST invoice layout for one tenant, and to keep a config value out of a deploy. You do not need a SaaS SDK that phones home, bills per seat, and becomes a single point of failure in front of your own Lucknow origin. A Postgres table, a short TTL cache in each process, and an admin path that only your operators can hit will carry a VPS fleet a long way.

Flags Are Data, Deploys Are Artifacts

A deploy should move code. A flag should move behaviour. If turning off checkout requires a git revert and a systemd restart, you have coupled release risk to product risk. Store flags next to the tenant they apply to. Default deny: a missing flag is off. That way a new node that has not yet synced still fails closed rather than exposing a half-built route.

sql
CREATE TABLE feature_flags (
  key          text NOT NULL,
  tenant_id    text NOT NULL DEFAULT '*',
  enabled      boolean NOT NULL DEFAULT false,
  payload      jsonb NOT NULL DEFAULT '{}',
  updated_at   timestamptz NOT NULL DEFAULT now(),
  updated_by   text NOT NULL,
  PRIMARY KEY (key, tenant_id)
);

The star tenant_id is the global default. A more specific row wins. payload holds small structured config such as a percentage rollout or a cap. Do not put secrets here. Flags are copied into every web process; secrets belong in files those processes cannot list.

Cache Hard, Invalidate Cheap

Reading Postgres on every HTTP request is how you turn a flags table into a latency budget. Load all flags for the node into memory on boot, then refresh every 15 to 30 seconds. That is an RTO for behaviour change, not for data. If you need a flag to flip in one second, send a reload signal or PUBLISH a tiny Redis message that only means "refetch". Do not stream the flag values through Redis as the source of truth; the table is.

python
import threading, time, psycopg

class FlagCache:
    def __init__(self, dsn, interval=20):
        self.dsn = dsn
        self.interval = interval
        self._flags = {}
        self._lock = threading.Lock()
        self.refresh()
        t = threading.Thread(target=self._loop, daemon=True)
        t.start()

    def refresh(self):
        with psycopg.connect(self.dsn) as conn:
            rows = conn.execute(
                'SELECT key, tenant_id, enabled, payload FROM feature_flags'
            ).fetchall()
        table = {}
        for key, tenant_id, enabled, payload in rows:
            table.setdefault(key, {})[tenant_id] = (enabled, payload)
        with self._lock:
            self._flags = table

    def enabled(self, key, tenant_id):
        with self._lock:
            by_tenant = self._flags.get(key) or {}
        row = by_tenant.get(tenant_id) or by_tenant.get('*')
        return bool(row and row[0])

    def _loop(self):
        while True:
            time.sleep(self.interval)
            try:
                self.refresh()
            except Exception:
                pass

Swallowing refresh errors is deliberate. A flags database blip must not restart your API. Stale flags for 30 seconds beat a 502. Log the failure and alert if refreshes miss two intervals. The cache is a copy, not a second product.

Who May Flip a Flag

Treat the admin endpoint as production access. mTLS from the jump host, or SSH port-forward to a loopback-only service. Do not put a public "feature console" on the internet with a shared password. Record updated_by. A flag flip is an audit event: who, what, when, previous value. Rollbacks are another UPDATE, not a deploy.

Percentage rollouts belong in payload. Hash tenant_id, take modulo 100, compare to the stored percent. Sticky by tenant, never by request, or a user will flap between layouts. When the percent hits 100, delete the special case and make the code path the default in the next release, then remove the flag. Flags that live forever are undeclared config and they rot.

Flags live in Postgres, copies live in process Operator loopback admin feature_flags source of truth API node cache refresh 20s Worker cache same table Request path memory lookup no vendor SDK fail closed

What Not to Flag

Do not flag schema migrations. A column is there or it is not; a flag cannot hide a missing table. Do not flag security controls. Authz that can be flipped from an admin UI is an incident waiting for a tired operator. Do not flag secrets, hostnames, or database URLs. Those are environment and files, different layer, different rotation story.

Keep the flag catalogue small. If you have 200 keys, you have a configuration product you are not staffing. Review flags monthly. Delete the ones whose code path is now the default. The operational win is a short list you can hold in your head during an incident, not a second CMS.

Workers must see the same table. A billing worker that ignores the halt-checkout flag will keep sending invoices after you thought you stopped the world. Point every role at the same DSN and the same cache interval so a flip is a fleet event, not a web-only event.

Takeaway

Self-hosted flags are a table, a cache, and an audited UPDATE. They let you change behaviour without a deploy and without a SaaS outage in someone else's region. Fail closed, cache on the node, and keep secrets out of the payload.

You can host the flags database on a Netbay Ubuntu VPS in Lucknow (DC01) and point every app node at it without adding a vendor to the request path — netbayhosts.in.

Keep reading

Follow along on a real VPS

Deploy Linux in under 60 seconds

These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.

Deploy an instance