Go (Gin) API backend for Stellabill - subscription and billing plans API. This repo is backend-only; a separate frontend consumes these APIs.
- Tech stack
- What this backend provides (for the frontend)
- Background Worker
- Local setup
- Configuration
- Testing
- API reference
- Database migrations
- Contributing (open source)
- Project layout
- API Contract & OpenAPI
- License
- Language: Go 1.22+
- Framework: Gin
- Database: PostgreSQL with Outbox Pattern for reliable event publishing
- Config: Environment variables (no config files required for default dev)
This service is the backend only. A separate frontend (or any client) can:
- Health check -
GET /api/healthto verify the API is up. - Plans -
GET /api/plansto list billing plans (id, name, amount, currency, interval, description). Currently returns an empty list; DB integration is planned. - Subscriptions -
GET /api/subscriptionsto list subscriptions andGET /api/subscriptions/:idto fetch one. Responses include plan_id, customer, status, amount, interval, next_billing. Currently placeholder/mock data; DB integration is planned.
CORS is enabled for all origins in development so a frontend on another port or domain can call these endpoints.
The backend includes a production-ready background worker system for automated billing job scheduling and execution.
- Job Scheduling: Schedule billing operations (charges, invoices, reminders) with configurable execution times
- Distributed Locking: Prevents duplicate processing when running multiple worker instances
- Retry Policy: Automatic retry with exponential backoff (1s, 4s, 9s) for failed jobs
- Dead-Letter Queue: Failed jobs after max attempts are moved for manual review
- Graceful Shutdown: Workers complete in-flight jobs before shutting down
- Metrics Tracking: Monitor job processing statistics (processed, succeeded, failed, dead-lettered)
- Concurrent Workers: Multiple workers can run safely without duplicate processing
internal/worker/README.md- Complete worker documentationinternal/worker/INTEGRATION.md- Integration guide with examplesinternal/worker/SECURITY.md- Security analysis and threat modelWORKER_IMPLEMENTATION.md- Implementation summary
import "stellarbill-backend/internal/timeutil"
store := worker.NewMemoryStore()
executor := worker.NewBillingExecutor()
config := worker.DefaultConfig()
w := worker.NewWorker(store, executor, config)
w.Start()
defer w.Stop()
scheduler := worker.NewScheduler(store)
job, _ := scheduler.ScheduleCharge("sub-123", timeutil.NowUTC(), 3)- Go 1.22 or later
- Check:
go version - Install: https://go.dev/doc/install
- Check:
- Git (for cloning and contributing)
- PostgreSQL (optional for now; app runs without it using default config; DB will be used when persistence is added)
git clone https://github.com/YOUR_ORG/stellabill-backend.git
cd stellabill-backendgo mod downloadCreate a .env file in the project root (do not commit it; it is in .gitignore):
# Required for startup
ENV=development
PORT=8080
DATABASE_URL=postgres://localhost/stellarbill?sslmode=disable
JWT_SECRET=ChangeMeNow123!Secure
ADMIN_TOKEN=AnotherStrongToken123!
# Required in production/staging (comma-separated https origins)
ALLOWED_ORIGINS=https://app.example.com,https://admin.example.com
# Optional with validation
RATE_LIMIT_ENABLED=true
RATE_LIMIT_MODE=ip
RATE_LIMIT_RPS=10
RATE_LIMIT_BURST=20
RATE_LIMIT_WHITELIST=/api/health
READ_TIMEOUT=30
WRITE_TIMEOUT=30
IDLE_TIMEOUT=120
MAX_HEADER_BYTES=1048576
AUDIT_HMAC_SECRET=stellarbill-dev-audit
AUDIT_LOG_PATH=audit.logOr export them in your shell. The app now fails fast when required values are missing or insecure.
go run ./cmd/serverServer listens on http://localhost:8080 (or the port you set via PORT).
curl http://localhost:8080/api/health
# Expected: {"service":"stellarbill-backend","status":"ok","outbox":{"pending_events":0,"dispatcher_running":true,"database_health":"healthy"}}
curl http://localhost:8080/api/outbox/stats
# Expected: {"pending_events":0,"dispatcher_running":true,"database_health":"healthy"}
curl -X POST http://localhost:8080/api/outbox/test
# Expected: {"message":"Test event published successfully","event_type":"test.event"}| Variable | Default | Description |
|---|---|---|
ENV |
development |
Environment (e.g. production) |
PORT |
8080 |
HTTP server port |
DATABASE_URL |
None (required) | PostgreSQL connection string (must be valid URL) |
JWT_SECRET |
None (required) | Secret for JWT (minimum 12 chars with upper/lower/digit/special) |
ADMIN_TOKEN |
None (required) | Admin endpoint token (minimum 12 chars with upper/lower/digit/special) |
ALLOWED_ORIGINS |
Required in production/staging |
Comma-separated https:// origins for CORS |
RATE_LIMIT_MODE |
ip |
One of: ip, user, hybrid |
RATE_LIMIT_RPS |
10 |
Integer between 1 and 1000 |
RATE_LIMIT_BURST |
20 |
Integer between 1 and 5000, must be >= RATE_LIMIT_RPS |
READ_TIMEOUT |
30 |
Timeout in seconds, range 1 to 3600 |
WRITE_TIMEOUT |
30 |
Timeout in seconds, range 1 to 3600 |
IDLE_TIMEOUT |
120 |
Timeout in seconds, range 1 to 3600 |
MAX_HEADER_BYTES |
1048576 |
Header size in bytes, range 1024 to 16777216 |
FF_DEFAULT_ENABLED |
false |
Default state for unknown flags |
FF_LOG_DISABLED |
true |
Log when flags block requests |
FF_CONFIG_FILE |
"" |
Path to feature flags config file |
Feature flags can be configured using environment variables in several ways:
Use the FF_ prefix for individual flags:
# Enable/disable specific features
FF_SUBSCRIPTIONS_ENABLED=true
FF_PLANS_ENABLED=false
FF_NEW_BILLING_FLOW=true
FF_ADVANCED_ANALYTICS=falseUse the FEATURE_FLAGS environment variable for bulk configuration:
export FEATURE_FLAGS='{"subscriptions_enabled": true, "plans_enabled": true, "new_billing_flow": false}'The system uses the following priority (highest to lowest):
FF_*individual environment variablesFEATURE_FLAGSJSON configuration- Default flag values
| Flag Name | Default | Description |
|---|---|---|
subscriptions_enabled |
true |
Enable subscription management endpoints |
plans_enabled |
true |
Enable billing plans endpoints |
new_billing_flow |
false |
Enable new billing flow feature |
advanced_analytics |
false |
Enable advanced analytics endpoints |
In production, set these via your host's environment or secrets manager; do not commit secrets.
import "stellarbill-backend/internal/middleware"
import "stellarbill-backend/internal/featureflags"
// Method 1: Middleware (recommended for endpoints)
router.GET("/feature", middleware.FeatureFlag("my_feature"), handler)
// Method 2: With default value
router.GET("/feature", middleware.FeatureFlagWithDefault("my_feature", true), handler)
// Method 3: Direct check in code
if featureflags.IsEnabled("my_feature") {
// Feature code here
}
// Method 4: Multiple flags requirement
router.GET("/feature", middleware.RequireAllFeatureFlags("flag1", "flag2"), handler)
router.GET("/feature", middleware.RequireAnyFeatureFlags("flag1", "flag2"), handler)See docs/dev-test-guide.md for the full local development and test execution guide, including common failure troubleshooting.
Unit tests cover config validation, service logic, HTTP handler behaviour, circuit breaker, and the background worker. They use in-memory mocks and require no external services.
go test ./internal/... -count=1 -timeout 60sIntegration tests spin up a real ephemeral Postgres container via Docker and validate the full request path — from route handler through service and repository to the database — then tear the container down automatically.
Prerequisites: Docker must be running locally (or in CI with Docker socket access). No manual database setup is required.
go test -tags integration -v -race -count=1 -timeout 120s ./integration/...The test suite in integration/ covers:
| Scenario | Expected |
|---|---|
| Owner fetches own active subscription | 200 with full plan + billing envelope |
| Unknown subscription ID | 404 |
| Soft-deleted subscription | 410 |
| Caller does not own the subscription | 403 |
Missing Authorization header |
401 |
| Malformed JWT | 401 |
| Subscription exists but referenced plan is missing | 200 with "plan not found" warning |
| Subscription has non-numeric amount | 500 |
| 10 concurrent reads of the same subscription | all 200, no data race |
GET /api/health |
200 |
GET /api/plans |
200 |
GET /api/subscriptions |
200 |
Migration timing and startup race handling: TestMain applies all SQL
migrations before any test runs. The Postgres container wait strategy requires
the ready-to-accept-connections log line to appear twice (once during
recovery init, once when actually ready), preventing false-positive startup
races.
CI example:
- name: Integration tests
run: go test -tags integration -race -count=1 -timeout 120s ./integration/...Base URL (local): http://localhost:8080
| Method | Path | Feature Flag Required | Description |
|---|---|---|---|
| GET | /api/health |
None | Health check |
| GET | /api/plans |
plans_enabled (default: true) |
List billing plans |
| GET | /api/subscriptions |
subscriptions_enabled (default: true) |
List subscriptions |
| GET | /api/subscriptions/:id |
subscriptions_enabled (default: true) |
Get one subscription |
| GET | /api/billing/new-flow |
new_billing_flow (default: false) |
New billing flow feature |
| GET | /api/analytics/advanced |
advanced_analytics AND subscriptions_enabled |
Advanced analytics |
All JSON responses. CORS allowed for * origin with common methods and headers.
Feature Flag Responses: When a feature flag blocks a request, the API returns:
{
"error": "feature_unavailable",
"message": "This feature is currently unavailable",
"feature_flag": "flag_name"
}Migrations live in migrations/ and are applied with:
go run ./cmd/migrate upSee docs/migrations.md for conventions and a production runbook.
Every push and pull request runs the following checks automatically via GitHub Actions (.github/workflows/ci.yml):
| Step | Command |
|---|---|
| Build | go build ./... |
| Vet | go vet ./... |
| Test + coverage | go test ./internal/... -covermode=atomic -coverpkg=./internal/... |
| Coverage threshold | ./scripts/check-coverage.sh coverage.out 95 (≥ 95 % on internal/) |
Coverage artifacts (coverage.out) are uploaded and retained for 14 days on every run.
# 1. Build
go build ./...
# 2. Vet
go vet ./...
# 3. Test with coverage (internal packages only — cmd/server is the process entrypoint)
go test ./internal/... \
-covermode=atomic \
-coverpkg=./internal/... \
-coverprofile=coverage.out \
-count=1 \
-timeout=60s
# 4. Enforce the 95 % threshold
./scripts/check-coverage.sh coverage.out 95
# 5. (Optional) Browse the HTML report
go tool cover -html=coverage.outWhy
./internal/...and not./...?
cmd/server/main.gois the process entry point (main()). Go cannot instrument it as a unit-testable package, so it always reports 0 % and would drag the total below the threshold. All business logic lives ininternal/, which is what the threshold enforces.
Security note: Never commit
.env, JWT secrets, or database credentials. The CI workflow contains no secrets; configure them via your host's environment or a secrets manager.
Recommended order for the HTTP chain:
recoveryrequest-idloggingcorsrate-limitauthfor protected routes only
Why this order:
recoverywraps the full chain so panics from downstream middleware and handlers are converted into structured500responses.request-idruns early so every response and log line can carry the same correlation ID.loggingruns before short-circuiting middleware so failed auth, rate-limit, and panic-recovery responses are still logged.corshandles preflightOPTIONSrequests before rate limiting or auth rejects them.rate-limitruns beforeauthon protected routes to reduce brute-force pressure on authentication logic.authshould be attached only to protected groups so public endpoints like/api/healthcan remain reachable.
Behavior verified by tests:
- Middleware entry and unwind order.
- Request ID propagation across middleware and handlers.
- Expected short-circuit responses for preflight, auth failures, rate limiting, and panic recovery.
Security notes:
X-Request-IDinput is sanitized before reuse in logs and responses.- The in-memory rate limiter is process-local and keyed by client IP, so deployments behind proxies should ensure trusted forwarding headers are configured correctly.
- CORS is currently configured as
*; production deployments should replace that with an explicit frontend origin. - The sample auth middleware validates a bearer token against the configured secret and is intended as a lightweight guard for protected groups until full JWT validation is introduced.
Handlers are constructed with explicit dependencies instead of reaching into package-level state. That keeps startup wiring easy to review and makes unit tests cheap to write because services can be replaced with focused mocks.
Current boundaries:
internal/servicesdefines the interfaces and default placeholder implementations used by the API.internal/handlersvalidates constructor input and translates service results into HTTP responses.internal/routesrequires an injected handler bundle and returns an error on nil wiring instead of registering a partially working router.cmd/serveris responsible for composing concrete services and failing fast if startup wiring is incomplete.
Security notes:
- Constructor validation prevents nil dependencies from reaching request handling paths, which avoids panic-driven denial of service during misconfigured startup.
- Route registration returns errors for missing router or handler wiring so invalid startup state fails closed.
- Service interfaces keep handlers decoupled from future storage implementations, making authorization and data-access checks easier to test in isolation.
- Tamper-evident chain: Each audit entry is HMAC-signed with
AUDIT_HMAC_SECRETand linked to the previous hash (chain-of-trust). Breaking or removing a line invalidates later hashes. - What gets logged:
actor,action,target,outcome, request method/path, client IP, and any supplied metadata (e.g., attempts, reasons). - Redaction: Sensitive fields such as tokens, passwords, secrets, Authorization headers, and values that look like bearer/basic credentials are stored as
[REDACTED]. - Sink: Default sink writes JSON Lines to
AUDIT_LOG_PATH(defaultaudit.log). File permissions are0600on creation. - Admin example:
POST /api/admin/purgedemonstrates a sensitive operation. Success, partial success (?partial=1), denied access, and retry attempts are all audit-logged. - Auth failures: 401/403 responses are automatically logged via middleware, with headers redacted.
Application logs now use newline-delimited JSON with a consistent schema for both HTTP middleware and outbox/retry paths.
- Canonical fields:
request_id,actor,tenant,route,status,duration_ms - Standard envelope: every entry also includes
ts,level, andmessage - Redaction rules: bearer/basic credentials, JWTs, emails, and fields such as
authorization,password,secret,token,cookie,payload,body, andevent_dataare redacted before write - Retry throttling: repeated outbox failures are emitted once per throttle window, with
suppressed_counton the next summary log so partial outages do not spam logs or inflate ingest costs - Safe payload handling: publishers log metadata like
payload_bytes,event_id, andevent_typeinstead of raw request/event bodies
Example request log:
{
"ts": "2026-04-24T12:00:00Z",
"level": "info",
"message": "request completed",
"request_id": "req-123",
"actor": "api-client",
"tenant": "tenant-42",
"route": "/protected",
"status": 200,
"duration_ms": 14,
"method": "GET"
}Example throttled retry log:
{
"ts": "2026-04-24T12:00:30Z",
"level": "warn",
"message": "outbox event scheduled for retry",
"request_id": "",
"actor": "system",
"tenant": "system",
"route": "outbox.dispatcher.retry",
"status": "retry_scheduled",
"duration_ms": 0,
"event_type": "subscription.created",
"retry_count": 2,
"suppressed_count": 17,
"error": "db down"
}Security assumptions:
- Request logs never include Authorization headers, cookies, request bodies, raw event payloads, or client IPs.
- If actor-like values contain emails or bearer-style tokens, the logger redacts them before serialization.
- Retry-loop logs are bounded by time window, which reduces noisy duplicate writes during downstream outages.
go test ./... -cover
Tests include redaction coverage, hash chaining, admin action logging, middleware logging, and outbox log throttling behaviour. Coverage currently exceeds 95% in CI for ./internal/....
We welcome contributions from the community. Below is a short guide to get you from "first look" to "merged change".
- Be respectful and inclusive.
- Focus on constructive feedback and clear, factual communication.
- Open an issue
- Bug: describe what you did, what you expected, and what happened.
- Feature: describe the goal and why it helps.
- Fork and clone
- Fork the repo on GitHub, then clone your fork locally.
- Create a branch
git checkout -b fix/your-fix # or feature/your-feature - Make changes
- Follow existing style (format with
go fmt). - Keep commits logical and messages clear (e.g. "Add validation for plan ID").
- Follow existing style (format with
- Run checks
Add or run tests if the project has them.
go build ./... go vet ./... go fmt ./...
- Commit
- Prefer small, atomic commits (one logical change per commit).
- Push and open a PR
git push origin fix/your-fix
- Open a Pull Request against the main branch.
- Fill in the PR template (if any).
- Link related issues.
- Describe what you changed and why.
- Review
- Address review comments. Maintainers will merge when everything looks good.
- Use the Local setup steps to run the server.
- Change code, restart the server (or use a tool like
airfor live reload if the project adds it). - Test with
curlor the frontend that consumes this API.
- Go:
go fmt,go vet, no unnecessary dependencies. - APIs: Keep JSON shape stable; document breaking changes in PRs.
- Secrets: Never commit
.env, keys, or passwords.
stellabill-backend/
├── .github/
│ └── workflows/
│ └── ci.yml # CI: build, vet, test, coverage threshold
├── cmd/
│ └── server/
│ └── main.go # Entry point, Gin router, server start
├── docs/
│ ├── outbox-pattern.md # Outbox pattern documentation
│ └── security-notes.md # Security considerations
├── internal/
│ ├── config/
│ │ └── config.go # Loads ENV, PORT, DATABASE_URL, JWT_SECRET, feature flags
│ ├── featureflags/
│ │ ├── featureflags.go # Feature flag management system
│ │ └── featureflags_test.go # Unit tests for feature flags
│ ├── middleware/
│ │ ├── featureflags.go # Feature flag middleware for endpoint gating
│ │ └── featureflags_test.go # Middleware tests
│ ├── handlers/
│ │ ├── health.go # GET /api/health (includes outbox status)
│ │ ├── plans.go # GET /api/plans
│ │ └── subscriptions.go # GET /api/subscriptions, /api/subscriptions/:id
│ ├── routes/
│ │ └── routes.go # Registers routes and CORS middleware
│ ├── service/
│ │ └── subscription_service.go # Business logic — ownership, soft-delete, billing
│ ├── testutil/
│ │ └── db.go # Ephemeral container lifecycle helpers
│ └── worker/
│ ├── job.go # Job model and JobStore interface
│ ├── store_memory.go # In-memory JobStore implementation
│ ├── worker.go # Background worker with scheduler loop
│ ├── executor.go # Billing job executor
│ └── scheduler.go # Job scheduling utilities
├── migrations/
│ ├── migrations.go # embed.FS export for the SQL files
│ ├── 001_create_plans.sql
│ └── 002_create_subscriptions.sql
├── go.mod
├── go.sum
└── README.md
- Environment Variables: Feature flags are configured via environment variables, which are secure and not committed to version control
- Default Behavior: Unknown flags default to
falsefor security (fail-safe) - No Dynamic Loading: Flags are loaded at startup only, preventing runtime injection attacks
- Thread Safety: All flag operations are thread-safe with proper mutex locking
- Validation: Invalid flag values are safely ignored and logged
- Production Flags: Always set explicit flag values in production; don't rely on defaults
- Secret Management: Use your cloud provider's secret manager for sensitive flag configurations
- Monitoring: Monitor flag usage and access patterns
- Audit Trail: Flag changes are tracked with timestamps for auditing
- Testing: Test both enabled and disabled states in your test suite
The feature flag system includes comprehensive tests covering:
- Concurrent access and race conditions
- Invalid input handling
- Memory leak prevention
- Environment variable injection attempts
- Edge cases and error conditions
Run tests with: go test ./...
This project follows a spec-first approach using OpenAPI 3.0.3. The specification is maintained in openapi/openapi.yaml and serves as the source of truth for all /api/* routes.
- Contract Tests: Automatically validate that implementation matches the spec (
go test ./internal/contract/...). - CI Enforcement: Pull requests are checked for undocumented endpoints via
go run ./cmd/openapi-validate. - Versioning: Versioned endpoints use
/api/v1/prefix; unversioned public endpoints (like health) stay at/api/. - Contributor Checklist: See
docs/OPENAPI_GUIDE.mdfor the full checklist when modifying API endpoints.
# Validate OpenAPI spec and check for undocumented routes
go run ./cmd/openapi-validate
# Run contract tests
go test ./internal/contract/... -v
# Run all tests with coverage
go test ./... -coverSee the LICENSE file in the repository (if present). If none, assume proprietary until stated otherwise. "# Test"