Skip to content
Get started
Core concepts

Authentication

Secure your Hub API with Bearer token authentication.

Hub requires Bearer token authentication. All API endpoints require the Authorization header with a Bearer token, except the health check endpoint, which is public.

Add API_KEY to your .env file:

Terminal window
API_KEY=your-secret-key-here
Terminal window
# macOS/Linux
openssl rand -base64 32
# Or use a password manager's random generator
Terminal window
make run

All API requests except GET /health require the Authorization header:

Terminal window
curl -H "Authorization: Bearer your-secret-key-here" \
http://localhost:8080/v1/feedback-records

These endpoints require authentication:

  • POST /v1/feedback-records - Create feedback record
  • GET /v1/feedback-records - List feedback records
  • GET /v1/feedback-records/{id} - Get feedback record
  • PATCH /v1/feedback-records/{id} - Update feedback record
  • DELETE /v1/feedback-records/{id} - Delete feedback record
  • DELETE /v1/feedback-records?user_identifier=<id> - Bulk delete feedback records by user

Always public (no auth required):

  • GET /health - Health check

The OpenAPI spec is maintained in the repo as openapi.yaml and used for contract tests (Schemathesis). Serving it at runtime (for example, GET /openapi.json or GET /docs) is planned.

Client Request
|
Has Authorization header?
|
No -> 401 Unauthorized
Yes -> Is format "Bearer <token>"?
|
No -> 401 Unauthorized
Yes -> Token matches API_KEY?
|
No -> 401 Unauthorized
Yes -> Process Request
|
200 Success

Without authentication:

Terminal window
curl http://localhost:8080/v1/feedback-records

Response:

{
"title": "Unauthorized",
"status": 401,
"detail": "Missing Authorization header"
}

With authentication:

Terminal window
curl -H "Authorization: Bearer your-secret-key-here" \
http://localhost:8080/v1/feedback-records

Response:

{
"data": [...],
"total": 10
}

Hub’s built-in Bearer token authentication is ideal for:

  • Internal services and tools
  • Server-to-server communication
  • Quick production deployments

For public APIs or multi-tenant systems, consider deploying behind an API gateway (AWS API Gateway, Kong, Traefik) for advanced features like OAuth, JWT, or per-user access control.

Terminal window
docker run -d \
-p 8080:8080 \
-e DATABASE_URL=postgres://... \
-e API_KEY=your-secret-key-here \
ghcr.io/formbricks/hub:latest
Terminal window
# Database connection
DATABASE_URL=postgres://user:pass@host/db
# Authentication (required)
API_KEY=your-secret-key-here
# Optional
LOG_LEVEL=info
PORT=8080

See full environment variable reference ->

Always use HTTPS to encrypt tokens in transit:

http://api.example.com (insecure)
https://api.example.com (secure)

Good practices:

  • Use environment variables (not hardcoded)
  • Store in secret management (AWS Secrets Manager, HashiCorp Vault)
  • Rotate keys regularly

Avoid:

  • Committing keys to Git
  • Sharing keys via email/Slack
  • Using the same key for dev and prod

Schedule regular key rotation (for example, every 90 days):

  1. Generate new key
  2. Update API_KEY in production
  3. Restart service
  4. Update clients with new key

Enable structured logging to track failed auth attempts:

Terminal window
LOG_LEVEL=info

Look for 401 responses in logs:

{
"level": "warn",
"msg": "unauthorized request",
"path": "/v1/feedback-records",
"ip": "203.0.113.1"
}

Symptom:

{
"title": "Unauthorized",
"status": 401,
"detail": "Missing Authorization header"
}

Solutions:

  1. Check if API_KEY is set:
Terminal window
grep API_KEY .env
  1. Verify header format:
Terminal window
# Correct
curl -H "Authorization: Bearer your-key" http://...
# Wrong (missing Bearer)
curl -H "Authorization: your-key" http://...
# Wrong (using X-API-Key instead)
curl -H "X-API-Key: your-key" http://...
  1. Check key matches .env:
Terminal window
cat .env | grep API_KEY

Health checks (/health) should never require auth.

If you see this, it’s a bug. Please report it.

If you add a /docs or /openapi.json endpoint to serve the API spec, it should remain public (no auth).

Terminal window
curl -H "Authorization: Bearer abc123" \
-X POST http://localhost:8080/v1/feedback-records \
-H "Content-Type: application/json" \
-d '{"source_type":"survey","field_id":"q1","field_type":"text","value_text":"Great!"}'
const response = await fetch("http://localhost:8080/v1/feedback-records", {
headers: {
Authorization: "Bearer abc123",
"Content-Type": "application/json",
},
method: "POST",
body: JSON.stringify({
source_type: "survey",
field_id: "q1",
field_type: "text",
value_text: "Great!",
}),
});
import requests
headers = {
"Authorization": "Bearer abc123",
"Content-Type": "application/json",
}
response = requests.get(
"http://localhost:8080/v1/feedback-records",
headers=headers
)
client := &http.Client{}
req, _ := http.NewRequest("GET", "http://localhost:8080/v1/feedback-records", nil)
req.Header.Set("Authorization", "Bearer abc123")
resp, err := client.Do(req)