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
- API reference
- Database migrations
- Contributing (open source)
- Project layout
- 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
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", time.Now(), 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):
# Optional - defaults shown
ENV=development
PORT=8080
DATABASE_URL=postgres://localhost/stellarbill?sslmode=disable
JWT_SECRET=change-me-in-production
ADMIN_TOKEN=change-me-admin-token
AUDIT_HMAC_SECRET=stellarbill-dev-audit
AUDIT_LOG_PATH=audit.logOr export them in your shell. The app will run with the defaults if you do not set anything.
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 |
postgres://localhost/stellarbill?sslmode=disable |
PostgreSQL connection string |
JWT_SECRET |
change-me-in-production |
Secret for JWT (change in prod) |
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)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.
go test ./... -cover
Tests include redaction coverage, hash chaining, admin action logging, and middleware auth-failure logging. Coverage currently exceeds 95%.
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
│ └── 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
│ ├── *_test.go # Comprehensive test suite (95%+ coverage)
│ ├── README.md # Worker documentation
│ ├── SECURITY.md # Security analysis and threat model
│ └── INTEGRATION.md # Integration guide with examples
├── go.mod
├── go.sum
├── .gitignore
├── README.md
└── WORKER_IMPLEMENTATION.md # Implementation summary
- 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 ./...
See the LICENSE file in the repository (if present). If none, assume proprietary until stated otherwise.