Settings
Workspace RBAC, session login, DSN keys, and webhook create/test curls for Epure self-host.
Workspace settings (you / team) and project settings (DSN, retention, webhooks). Dashboard APIs need a session; Postgres RLS scopes rows with app.current_org_id.
How do I sign in?
Email and password at /login, or Google when GOOGLE_CLIENT_ID is set. Dashboard /api/v1/* needs the resulting cookie.
Billing tab
/billing is a Cloud placeholder. OSS ops: ignore it. Cloud later: Pro $24/mo, Plus $79/mo.
How do I open a session?
Password login returns 204 and Set-Cookie (Epure.sid on local HTTP). DSN headers on these routes return 401.
Sign in with email
Seed user after ./scripts/seed-dev.sh. Register at /login in production.
command
curl -sS -c cookies.txt -D - -o /dev/null -X POST \
"http://localhost:8080/api/v1/auth/login" \
-H "Content-Type: application/json" \
-d '{"email":"dev@epure.local","password":"devpassword"}'Expected
HTTP/1.1 204 No Content Set-Cookie: Epure.sid=...; HttpOnly; Path=/; SameSite=Lax
| Result | What happened | What to do |
|---|---|---|
| 204 + cookie | Session established | Reuse -b cookies.txt on /api/v1/* |
| 401 | Bad email or password | Register, or re-run seed-dev |
| 429 plain text | 20 login/register req / IP / 60s | Wait |
Later 401 on /api/v1 | Cookie rejected (Secure vs HTTP) | Local HTTP: EPURE_SESSION_SECURE=0. Troubleshooting |
Full auth table: Authentication.
What is on workspace settings?
| Route | Purpose |
|---|---|
/settings | Profile / general account |
/settings/security | Password and sessions |
/team | Members, invites, roles |
/usage | Usage overview |
/billing | Cloud placeholder |
Sign-in at /login: email + password (argon2id), or Google when GOOGLE_CLIENT_ID is set.
Session cookies: Epure.sid on HTTP (EPURE_SESSION_SECURE=0), __Host-epure.sid behind HTTPS (EPURE_SESSION_SECURE=1).
Roles (RBAC lite)
| Role | Can |
|---|---|
| Owner | Everything Admin can + delete projects, full workspace control |
| Admin | Projects, DSN keys, webhooks, invites, project settings |
| Member | View and triage issues only |
Invite by email from /team. Invitees register or sign in with Google; the assigned role applies on join.
| Symptom | What happened | What to do |
|---|---|---|
| Admin tabs read-only | Session role is Member | Owner/Admin invites you again with Admin |
| 403 on DSN/webhook routes | require_min_role(Admin) rejected | Sign in as Admin or Owner |
| Invitee lands as Member | Invite carried Member | Patch role from /team as Owner/Admin |
What is on project settings?
Base path: /p/:projectId/settings
| Tab | Route | Who |
|---|---|---|
| General | /p/:projectId/settings | Admin / Owner |
| DSN keys | /p/:projectId/settings/dsn | Admin / Owner |
| Webhooks | /p/:projectId/settings/webhooks | Admin / Owner |
DSN keys
Create, rotate, revoke. Revoked keys → ingest 403 dsn_revoked. UI walkthrough: Projects.
Create a DSN key
command
curl -sS -b cookies.txt -X POST \
"http://localhost:8080/api/v1/projects/${PROJECT_ID}/dsn-keys" \
-H "Content-Type: application/json" \
-d '{"label":"local-dev"}'Expected
HTTP/1.1 201 Created
{ "id": "<key-uuid>", "project_id": "<project-uuid>", "public_key": "a1b2…", "secret_key": "<shown once>", "label": "local-dev" }Copy secret_key immediately. List responses omit it.
Webhooks
Destinations: Slack (format: "slack"), Discord (discord), or custom HTTPS (generic).
| Event id | When |
|---|---|
issue_created | First event for a fingerprint |
regression | Resolved issue returns in a newer release |
URLs must be public HTTPS. Private/loopback targets require EPURE_WEBHOOK_ALLOW_PRIVATE=1 (dev/tests only).
Create a generic webhook
command
curl -sS -b cookies.txt -X POST \
"http://localhost:8080/api/v1/webhooks" \
-H "Content-Type: application/json" \
-d '{"project_id":"550e8400-e29b-41d4-a716-446655440000","url":"https://hooks.example.com/epure","format":"generic","events":["issue_created","regression"]}'Expected
HTTP/1.1 201 Created
{ "id": "<webhook-uuid>", "url": "https://hooks.example.com/epure", "format": "generic", "events": ["issue_created","regression"], "signing_secret": "<shown once>" }Test fire: POST /api/v1/webhooks/{id}/test with the same cookie → { "sent": true }.
| Result | What happened | What to do |
|---|---|---|
| 400 on create | URL failed SSRF checks, or bad format/events | Public HTTPS; format in slack/discord/generic; events issue_created or regression |
| Hook never arrives | Private URL blocked | Do not set EPURE_WEBHOOK_ALLOW_PRIVATE=1 in prod |
How does RLS apply for operators?
- Dashboard never trusts a client-supplied org id; membership on the session sets
app.current_org_id - Ingest does not use the app RLS role; it authenticates by DSN and writes for that project
- Three URLs:
DATABASE_URL(migrations),EPURE_INGEST_DATABASE_URL,EPURE_APP_DATABASE_URL; see Configuration
Checklist
- At least one Owner exists
- Production uses HTTPS +
EPURE_SESSION_SECURE=1 - DSN keys created; secrets stored outside git
- CORS origins restricted (
EPURE_CORS_ORIGINS) for browser SDKs - Webhooks pointed at public endpoints
- Retention and ingest caps set per project
Why can a Member not rotate a DSN?
Rotate needs Admin or Owner; Members view and triage issues only. Ask an Owner to patch the role from /team. DSN create, rotate, and revoke sit on /p/:projectId/settings/dsn. Revoked keys then return 403 dsn_revoked on ingest.
Why is the Google button missing?
GOOGLE_CLIENT_ID is empty, so the login page hides Google. Set the id, secret, and a redirect URI matching {EPURE_PUBLIC_URL}/api/v1/auth/google/callback, then restart epure. Password login still works: Configuration.
Is a DSN the same as a session?
No. The DSN authenticates ingest only. Settings, Issues, and Events APIs use the login cookie and RLS. Sending DSN headers to /api/v1/* returns 401, and sending a cookie to envelope or store does nothing. See Authentication.
Next
- Projects: DSN keys and retention
- Authentication: login 204 + cookie
- Configuration: OAuth and session cookies
- Troubleshooting: login loops and CORS