A self-hosted feature flag platform built on GO Feature Flag. It provides a relay proxy for flag evaluation and a lightweight web UI to improve DX when managing boolean feature flags.
This repository is also a template for deploying the same architecture in your own GCP project. See Using This as a Template to get started.
graph TB
subgraph Internet
browser["Browser Client<br/>(flag management UI)"]
app["Application Client<br/>(flag evaluation)"]
end
subgraph "Google Cloud – your-gcp-project-id / us-east1"
subgraph "Web Service ALB"
web_fwd["Forwarding Rule :443"]
web_https["HTTPS Proxy<br/>+ Managed SSL"]
web_urlmap["URL Map"]
web_iap["Identity-Aware Proxy<br/>(@yourdomain.com only)"]
web_backend["Backend Service"]
web_neg["Serverless NEG"]
end
subgraph "Relay Proxy ALB"
relay_fwd["Forwarding Rule :443"]
relay_https["HTTPS Proxy<br/>+ Managed SSL"]
relay_urlmap["URL Map"]
relay_armor["Cloud Armor<br/>60 req/min per IP"]
relay_backend["Backend Service"]
relay_neg["Serverless NEG"]
end
subgraph "Cloud Run"
web["ff-web-service<br/>(Go + Svelte SPA)<br/>SA: ff-web-service"]
proxy["ff-relay-proxy<br/>(GO Feature Flag)<br/>SA: ff-relay-proxy"]
end
gcs[("GCS: your-flags-bucket<br/>flags.goff.yaml")]
slack["Slack Webhook<br/>(flag change notifications)"]
end
browser -->|HTTPS| web_fwd
web_fwd --> web_https --> web_urlmap --> web_iap --> web_backend --> web_neg --> web
app -->|HTTPS| relay_fwd
relay_fwd --> relay_https --> relay_urlmap --> relay_armor --> relay_backend --> relay_neg --> proxy
web -->|"read/write<br/>(objectAdmin)"| gcs
proxy -->|"read<br/>(objectViewer)"| gcs
proxy -->|notifications| slack
style web_iap fill:#f59e0b,stroke:#d97706,color:#000
style relay_armor fill:#ef4444,stroke:#dc2626,color:#fff
style gcs fill:#4285f4,stroke:#1a73e8,color:#fff
| Principal | Resource | Role | Purpose |
|---|---|---|---|
ff-relay-proxy SA |
GCS your-flags-bucket |
storage.objectViewer |
Read flag config |
ff-web-service SA |
GCS your-flags-bucket |
storage.objectAdmin |
Read/write flag config |
ff-web-service SA |
Cloud Run ff-relay-proxy |
run.invoker |
Only authorized caller |
domain:yourdomain.com |
Web service backend | iap.httpsResourceAccessor |
UI access via IAP |
Flag evaluation (application clients):
- HTTPS request hits the relay proxy ALB
- Cloud Armor enforces 60 requests/minute per IP
- Request reaches the relay proxy Cloud Run service
- Relay proxy reads flag config from GCS (polled every 120s)
Flag management (browser):
- HTTPS request hits the web service ALB
- IAP requires Google authentication with an
@yourdomain.comemail - Request reaches the web service Cloud Run service
- Web service reads/writes flag config directly in GCS
Both Cloud Run services are configured with INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER — they cannot be accessed directly, only through their respective load balancers.
Wraps the official gofeatureflag/go-feature-flag Docker image with a config template that substitutes environment variables at startup via envsubst.
Environment variables:
GCS_BUCKET— GCS bucket nameGCS_OBJECT— path to the flags YAML file in the bucketSLACK_WEBHOOK_URL— Slack incoming webhook for change notificationsADMIN_API_KEY— API key for/admin/endpoints (evaluation endpoints are open)
A Go HTTP server that:
- Serves a prebuilt Svelte SPA (embedded in the binary via
go:embed) GET /api/flags— reads the GOFF YAML from GCS, returns all flags as JSONPUT /api/flags— updates a boolean flag in the GOFF YAML and writes it back to GCS
Environment variables:
GCS_BUCKET— GCS bucket nameGCS_OBJECT— path to the flags YAML filePORT— HTTP listen port (default8080)
Provisions all GCP infrastructure. See infra/variables.tf for required inputs.
Prerequisites:
- GCS bucket
your-tf-state-bucketmust exist (Terraform remote backend) - OAuth consent screen must be configured in GCP console (one-time, for IAP)
- Deploying principal needs
roles/iap.admin
cd infra
terraform init
terraform plan -var-file=prod.tfvars
terraform apply -var-file=prod.tfvarsAfter applying, point DNS records for the relay proxy and web service domains to the IP addresses in the Terraform outputs.
We provide 2 examples to help demonstrate how to interact with the relay proxy in both client and server-side applications.
- React: examples/react-client
- Go: examples/go-service
We use fake-gcs-emulator to enable a complete local test environment. The docker-compose file performs all setup required to run the relay proxy, web UI and GCS emulator.
docker compose up --build -dThen, interact with the various components:
- Web SPA: http://localhost:8080
- Relay proxy: http://localhost:1031
- Checkout the API docs at
http://localhost:1031/swagger/index.html
- Checkout the API docs at
- GCS emulator: http://localhost:4443
The Web UI exposes a simple interface for viewing all existing feature flags, and editing simple flags (only Boolean variants are supported at time of writing).
Flag updates in the UI are persisted to GCS; however, the tracked flag configuration is considered the source-of-truth: once a change to that file is committed to main, it will overwrite the existing state of the object in GCS. Therefore, if you alter a flag value in the UI and want it to stick around, you should alter the file directly and commit the change.
See the Go Feature Flag docs to learn about the other ways to create and interact with feature flags.
Since we don't currently support non-boolean flag updates through the web UI, the following procedure can be followed to update other flag values:
- Pre-requisite: Have object write access on
gs://your-flags-bucketand access togsutil - Make the desired changes to
flags/flags.goff.yaml - Run
gsutil cp flags/flags.goff.yaml gs://your-flags-bucket/flags.goff.yaml
We currently configure the relay proxy to poll for flag updates every 2 minutes. If you want to force an immediate refresh, you can interact with the relay proxy's admin refresh endpoint; e.g.,
curl -X 'POST' \
'http://<your_domain>:1031/admin/v1/retriever/refresh' \
-H 'accept: application/json' \
-H 'X-API-Key: <your_admin_api_key>' \For local testing, we've set the API key to local-dev-key in docker-compose.yaml.
This repository also exposes a goff-flags skill to make it easier to configure complex feature flags.
This repository is designed to be forked and adapted. The architecture diagram above reflects the full production setup. Follow the steps below to stand it up in your own GCP environment.
Search the codebase for the following placeholder values and replace them with your own:
| Placeholder | Where | Description |
|---|---|---|
your-gcp-project-id |
infra/variables.tf, infra/backend.tf, .gitlab-ci.yml |
Your GCP project ID |
your-tf-state-bucket |
infra/backend.tf |
GCS bucket for Terraform remote state |
your-flags-bucket |
infra/variables.tf, docker-compose.yaml, service/main.go |
GCS bucket for flag YAML storage |
yourdomain.com |
infra/variables.tf (iap_access_domain) |
Google Workspace domain for IAP access |
your-gar-repo |
.gitlab-ci.yml |
Artifact Registry repository name |
YOUR_PROJECT_NUMBER |
.gitlab-ci.yml |
Numeric GCP project number (from gcloud projects describe) |
YOUR_WIF_POOL_ID / YOUR_WIF_PROVIDER_ID |
.gitlab-ci.yml |
Workload Identity Federation pool/provider IDs |
Run the bootstrap script once to provision the GCS state bucket, Artifact Registry repository, Workload Identity Federation pool, and CI/CD service account:
./scripts/bootstrap-infra.sh \
YOUR_PROJECT_ID \
YOUR_PROJECT_NAME \
YOUR_GITLAB_GROUP \
admin@yourdomain.com \
https://gitlab.yourdomain.comThe script prints the exact GCP_WORKLOAD_IDENTITY_PROVIDER and GCP_SERVICE_ACCOUNT values to paste into .gitlab-ci.yml.
Before Terraform can enable IAP, you must configure an OAuth consent screen in the GCP console:
https://console.cloud.google.com/apis/credentials/consent?project=YOUR_PROJECT_ID
This is a manual one-time step per project. The deploying principal also needs roles/iap.admin.
The CI/CD pipeline reads the relay proxy admin API key from Secret Manager. Create it once:
echo -n "your-api-key" | gcloud secrets create ff-relay-proxy-admin-api-key \
--data-file=- \
--project=YOUR_PROJECT_IDAlso set the following as GitLab CI/CD variables (Settings → CI/CD → Variables):
SLACK_WEBHOOK_URL— Slack incoming webhook URLRELAY_PROXY_DOMAIN— public domain for the relay proxy (e.g.flags-api.yourdomain.com)WEB_SERVICE_DOMAIN— public domain for the web UI (e.g.flags.yourdomain.com)GCS_FLAGS_BUCKET— the GCS bucket name for flag storage
cd infra
terraform init
terraform apply -var="project_id=YOUR_PROJECT_ID" \
-var="flags_bucket_name=YOUR_FLAGS_BUCKET" \
-var="iap_access_domain=yourdomain.com" \
-var="relay_proxy_domain=flags-api.yourdomain.com" \
-var="web_service_domain=flags.yourdomain.com" \
-var="relay_proxy_image=..." \
-var="web_service_image=..." \
-var="slack_webhook_url=..." \
-var="relay_proxy_admin_api_key=..."Point DNS A records for both domains to the IP addresses printed in the Terraform outputs.
GitLab: An example pipeline is provided in .gitlab-ci.yml. It uses Workload Identity Federation (keyless auth) to authenticate to GCP — no long-lived service account keys required. On every push to main, it builds Docker images via Cloud Build, runs a Terraform plan, and applies if the plan succeeds. Flag config changes trigger a direct gsutil cp sync to GCS.
GitHub Actions: Example workflows will be added in a future update.