Request Flow Traces Chapter 08
Complete request lifecycle traces showing every layer a request passes through, step by step, with the actual code that executes. File paths reference the real codebase.
On this page
8.1 How to Read These Traces
Each trace below shows the complete path of a request through the system. Every numbered step represents actual code execution, with file paths you can open in your editor.
apps/ and pkg/. Code snippets are simplified for clarity but match the actual implementation. Cross-service calls (HTTP or Kafka) are explicitly marked as boundaries.
The gateway service sits in front of all requests. Depending on route configuration in apps/gateway-service/config/routes.yaml, the gateway either validates JWTs and injects tenant headers, or passes the request through directly. Individual services apply their own middleware chains on top.
8.2 TRACE 1: POST /api/v1/auth/login
The login flow is the most critical path in the system. It touches the gateway, auth-service, admin-service (cross-service HTTP), PostgreSQL, Redis, and JWT generation.
Step 1: Client sends request
The client sends a POST to the gateway with email and password.
POST https://acme.app.com:8080/api/v1/auth/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "secret123"
}
Step 2: Gateway middleware chain
apps/gateway-service/cmd/server/main.go:130-134
The request enters the gateway and passes through the middleware chain. Middleware is applied in reverse order (last .Use() is outermost).
// Applied bottom-to-top: tracing wraps everything, tenant is next, etc. var handlerChain http.Handler = mux handlerChain = middleware.CORSMiddleware(handlerChain) // 4. Sets CORS headers handlerChain = middleware.RequestIDMiddleware(handlerChain) // 3. Generates UUID, sets X-Request-ID handlerChain = middleware.TenantIdentifierMiddleware(baseDomain)(handlerChain) // 2. Extracts "acme" from Host, sets X-Subdomain handlerChain = tracing.Middleware(handlerChain) // 1. Creates OTel span
After this chain, request headers now include: X-Request-ID: <uuid>, X-Subdomain: acme, plus an active OpenTelemetry span.
Step 3: Gateway route matching & proxy
apps/gateway-service/internal/handler/router.go:29-49
The Router's ServeHTTP method matches the request path against routes.yaml using prefix matching.
func (rtr *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) { route := rtr.findRoute(r.URL.Path) // Path "/api/v1/auth/login" matches route: path="/api/v1/auth" // Config: auth_required=false, target_url="http://localhost:8081" if route.AuthRequired { // SKIPPED for /api/v1/auth — auth-service handles its own auth } rtr.proxyRequest(w, r, route) // → reverse proxy to http://localhost:8081 }
/api/v1/auth route has auth_required: false. This means the gateway does not validate JWTs for auth-service routes. The auth-service applies its own JWT middleware for protected endpoints like /users.
Step 4: Auth-service receives proxied request
apps/auth-service/cmd/server/main.go:133-138
The auth-service's Chi router matches the /login endpoint. This route sits outside the authenticated group, so no JWT middleware is applied.
r.Route("/api/v1/auth", func(r chi.Router) { r.Post("/login", authHandler.Login) // ← matched! No auth required r.Post("/refresh-token", authHandler.RefreshToken) // ... r.Group(func(ar chi.Router) { ar.Use(auth.JWTMiddleware(cfg.JwtCfg)) // JWT required for this group ar.Use(auth.TenantContextFromClaimsMiddleware) ar.Route("/users", func(r chi.Router) { ... }) }) })
Step 5: Handler decodes & validates
apps/auth-service/internal/handler/auth.go:20-46
The Login handler decodes JSON, validates fields, calls the service layer, and sets response cookies.
func (h *authHandler) Login(w http.ResponseWriter, r *http.Request) { var req model.AuthLogin if err := httpx.DecodeAndValidate(r, &req); err != nil { httpx.ValidationError(w, err) // 400 with field-level errors return } resp, err := h.svc.Login(&req, r.Context()) // → Step 6 if err != nil { httpx.HandleError(w, err) // maps to HTTP status via typed errors return } httpx.SetRefreshTokenCookie(resp.RefreshToken, w) // HttpOnly cookie httpx.SetTenantIdCookie(resp.TenantId, w) data := map[string]interface{}{ "user_id": resp.UserId, "token": resp.Token, "tenant_id": tenantID, "subdomain": resp.Subdomain, } json.NewEncoder(w).Encode(data) }
Step 6: Service — business logic
apps/auth-service/internal/service/auth_service.go:33-71
The service layer orchestrates the full login sequence: DB lookup, password verification, cross-service tenant fetch, and JWT generation.
func (s *authService) Login(p *model.AuthLogin, ctx context.Context) (*domain.LoginResponse, error) { user, err := s.repo.Login(p) // → Step 7: DB query if err != nil { return nil, err } err = bcrypt.CompareHashAndPassword( // Verify password []byte(user.Password), []byte(p.Password)) if err != nil { return nil, httpx.Unauthorized("invalid credentials") } if user.IsActive != nil && !*user.IsActive { return nil, httpx.Unauthorized("account is deactivated") } tenantData, err := s.GetTenantData(tenantId) // → Step 8: HTTP → admin-service tokenStr, refreshToken, err := s.repo.GetAccessToken(user, ctx, tenantData) // → Step 9 return &domain.LoginResponse{ UserId: user.ID.String(), Token: tokenStr, RefreshToken: refreshToken, TenantId: tenantId, Subdomain: tenantData["Subdomain"], }, nil }
Step 7: Repository — database query
apps/auth-service/internal/repository/auth_repo.go:35-47
GORM query with eager loading. Preloads the entire role/permission tree in a single operation.
func (r *authRepository) Login(p *model.AuthLogin) (*model.User, error) { var u model.User if err := r.db. Preload("Role.RoleMenu.Menu"). // Eager load: User → Role → []RoleMenu → Menu Where("email = ? or phone = ?", p.Email, p.Email). First(&u).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, httpx.Unauthorized("invalid credentials") } return nil, err } return &u, nil } // SQL generated (approx): // SELECT * FROM users WHERE email = $1 OR phone = $1 LIMIT 1 // SELECT * FROM roles WHERE id = $1 // SELECT * FROM role_menus WHERE role_id = $1 // SELECT * FROM menus WHERE id IN ($1, $2, ...)
Step 8: Cross-service call to admin-service
apps/auth-service/internal/service/auth_service.go:99-115
The auth-service calls admin-service over HTTP to fetch tenant metadata (subdomain, features). This is a synchronous service-to-service call.
func (s *authService) GetTenantData(tenantID string) (map[string]any, error) { url := s.internalUrl.AdminServiceUrl + "/me/tenants/" + tenantID // e.g. "http://localhost:8082/internal/me/tenants/abc-123" respAuth, err := httpx.DoJSON("GET", url, nil, "") var result map[string]any json.NewDecoder(respAuth.Body).Decode(&result) return result, nil // { "Subdomain": "acme", "Features": [...], ... } }
/internal/me/tenants/{id} is registered as an internal route with no auth required (service-to-service trust).
Step 9: JWT generation + Redis sync
apps/auth-service/internal/repository/auth_repo.go:170-278
Generates the access token (JWT with claims), creates a refresh token (UUID hashed with SHA-256), and syncs version numbers to Redis for real-time invalidation.
func (r *authRepository) GenerateAccessToken(user, ctx, tenantData) (string, error) { // Build permission map from user's role menus permissions := map[string][]string{ "broadcast": {"read", "create"}, "users": {"read", "create", "update", "delete"}, // ... built from Role.RoleMenu.Menu + Can* flags } claims := jwt.MapClaims{ "sub": user.ID, "tenant_id": user.TenantId, "role": roleName, "subdomain": tenantData["Subdomain"], "permissions": permissions, "role_id": user.RoleId, "token_version": tokenVersion, "role_version": roleVersion, "role_user_version": roleUserVersion, "tenant_feature": tenantFeature, "exp": time.Now().Add(r.accessTokenExpiry).Unix(), } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenStr, _ := token.SignedString([]byte(r.secret)) // Sync versions to Redis for real-time invalidation redisclient.SyncAuthVersion(ctx, "auth:token_version:", userId, tokenVersion) redisclient.SyncAuthVersion(ctx, "auth:role_version:", roleId, roleVersion) redisclient.SyncAuthVersion(ctx, "auth:role_user_version:", userId, roleUserVersion) return tokenStr, nil } // Refresh token: UUID → SHA-256 hash → stored in DB func (r *authRepository) AccessAndRefreshToken(...) { newRefresh := uuid.NewString() newHash := helpers.HashRefreshToken(newRefresh) newRT := model.RefreshToken{ID: uuid.New(), UserID: user.ID, TokenHash: newHash, ...} db.Create(&newRT) }
Step 10: Response
The response flows back through the reverse proxy to the client.
// HTTP Response: 200 OK // Set-Cookie: refresh_token=<uuid>; HttpOnly; Path=/ // Set-Cookie: tenant_id=<uuid>; Path=/ { "user_id": "550e8400-e29b-41d4-a716-446655440000", "token": "eyJhbGciOiJIUzI1NiIs...", "tenant_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "subdomain": "acme" }
8.3 TRACE 2: POST /api/v1/auth/users (Create User)
An authenticated flow that passes through gateway without JWT validation (auth-service handles its own), then through the auth-service's JWT middleware to create a new user.
| Step | Description |
|---|---|
| Step 1 | Client sends POST /api/v1/auth/users with Authorization: Bearer <token> and user JSON body. |
| Step 2 | Gateway middleware chain executes: tracing.Middleware → TenantIdentifierMiddleware → RequestIDMiddleware → CORSMiddleware. Same as login flow. |
| Step 3 | Gateway matches /api/v1/auth → auth_required: false. The gateway does not validate the JWT; it proxies the request directly to auth-service at http://localhost:8081. |
| Step 4 | Auth-service middleware: auth.JWTMiddleware(cfg.JwtCfg) validates the Bearer token, checks token signature (HS256), verifies expiration, compares token_version / role_version / role_user_version against Redis to detect revoked tokens. Injects decoded claims into request context.apps/auth-service/cmd/server/main.go:152-154 |
| Step 5 | auth.TenantContextFromClaimsMiddleware extracts tenant_id, sub (user ID), role, and subdomain from JWT claims and builds a TenantContext struct stored in request context via context.WithValue(). |
| Step 6 | Handler (user.go:28-53): Decodes the User JSON body with httpx.DecodeAndValidate. Calls auth.RequireTenantContext(r) to get the authenticated user's context. If the caller is a Platform Admin and the body includes tenant_id, that tenant is honored. Otherwise, the user's own tc.TenantID is used. |
| Step 7 | Service (user_service.go:60-85): Hashes password with bcrypt.GenerateFromPassword(DefaultCost), lowercases email with strings.ToLower, then calls repo.Create(user). |
| Step 8 | Repository (user_repo.go:32-73): Executes a GORM transaction. First creates the User row. If the user has ConfigID (config options), creates ConfigUsers join records. Catches PG error code 23503 (foreign key violation) and returns a conflict error. |
| Step 9 | Service post-processing: If SendMail flag is true, dispatches a set-password email via the background mail.EmailWorker channel. If BusinessNumberID is set, publishes a business_number.requested event to Kafka topic admin.events.v1 for the admin-service to consume. |
| Step 10 | Response: 201 Created with the created user JSON (password field stripped). |
8.4 TRACE 3: Webhook → Kafka → Worker
A complete asynchronous flow for WhatsApp message status updates. Meta sends a webhook, which gets validated, resolved to a tenant, published to Kafka, and consumed by the broadcast worker.
Step 1: Meta sends POST to webhook endpoint
Meta's WhatsApp Business API sends a webhook notification to our endpoint.
POST /api/v1/webhooks/meta
X-Hub-Signature-256: sha256=abc123def456...
Content-Type: application/json
{
"object": "whatsapp_business_account",
"entry": [{
"id": "waba_123456789",
"changes": [{
"field": "messages",
"value": {
"statuses": [{ "id": "wamid.xxx", "status": "delivered", ... }]
}
}]
}]
}
Step 2: meta-webhook-service handler
apps/meta-webhook-service/internal/handler/webhook_handler.go:86-138
The handler reads the raw body (required for HMAC verification), validates the signature, and parses the payload.
func (h *WebhookHandler) HandleWebhookEvent(w, r) { // 1. Read raw body for signature validation body, _ := io.ReadAll(r.Body) // 2. Validate HMAC-SHA256 signature with Meta app secret signature := r.Header.Get("X-Hub-Signature-256") if err := h.validator.Validate(signature, body); err != nil { http.Error(w, "Invalid signature", 401) return } // 3. Parse JSON payload var payload sharedModel.MetaWebhookPayload json.Unmarshal(body, &payload) // 4. Process each entry for _, entry := range payload.Entry { wabaID := entry.ID tenant, _ := h.tenantResolver.ResolveByWABAId(ctx, wabaID) // → Step 3 for _, change := range entry.Changes { h.processChange(ctx, tenant.TenantID, wabaID, &change, payload) // → Step 4 } } w.WriteHeader(200) // Always return 200 to Meta }
Step 3: Tenant resolution via admin-service
apps/meta-webhook-service/internal/service/tenant_resolver.go:40-68
Maps a WhatsApp Business Account (WABA) ID to a tenant ID by calling admin-service's internal endpoint.
func (r *TenantResolver) ResolveByWABAId(ctx, wabaID) (*TenantInfo, error) { url := fmt.Sprintf("%s/internal/waba/%s/tenant", r.adminServiceURL, wabaID) // → GET http://admin-service:8082/internal/waba/waba_123456789/tenant resp, _ := r.httpClient.Do(req) // Returns: { "tenant_id": "abc-123", "waba_id": "waba_123456789" } }
Step 4: Event publishing to Kafka
apps/meta-webhook-service/internal/service/event_publisher.go:138-171
Based on the webhook field type, the publisher creates an event and publishes to the appropriate Kafka topic.
// Route by change.Field: switch change.Field { case "message_template_status_update": // → topic: whatsapp.template.status.v1 // → event type: whatsapp.template.status_update.v1 case "messages": // Statuses → topic: whatsapp.message.status.v1 // → event type: whatsapp.message.status.v1 // Messages → topic: whatsapp.message.incoming.v1 } // Event envelope structure: event := eventbus.Event{ ID: uuid, Type: "whatsapp.message.status.v1", Source: "meta-webhook-service", TenantID: "abc-123", Data: MessageStatusPayload{...}, // JSON-serialized }
Step 5: Broadcast Worker consumes from Kafka
apps/broadcast-service/cmd/worker/main.go:266-276
The broadcast worker subscribes to multiple Kafka topics. Each message is deserialized into an Event envelope and routed to the appropriate handler.
// Worker subscribes to these topics: consumer.Subscribe("broadcasts.v1", groupID, handler) consumer.Subscribe("whatsapp.template.status.v1", groupID, metaTemplateHandler) consumer.Subscribe("whatsapp.message.status.v1", groupID, metaMessageStatusHandler) consumer.Subscribe("whatsapp.template.quality.v1", groupID, metaTemplateQualityHandler) // Message status handler: metaMessageStatusHandler := func(ctx, event) error { if event.Type == eventbus.EventTypeWhatsAppMessageStatus { metaConsumerSvc.HandleMessageStatusEvent(ctx, event) // Updates message delivery status in DB: sent/delivered/read/failed } }
Step 6: Handler processes status update
The MetaConsumerService updates the database record with the new delivery status. For template status updates, it updates the template's approval/rejection status.
// Template status: update template status in DB // APPROVED → template is ready to use // REJECTED → template needs revision, reason stored // Message status: update delivery status in broadcast_messages table // sent → delivered → read (progressive status) // failed → store error code and reason
Async Flow Sequence Diagram
Meta meta-webhook-svc admin-svc Kafka broadcast-worker DB
│ │ │ │ │ │
│──POST /webhooks/meta→│ │ │ │ │
│ │ │ │ │ │
│ │ Validate HMAC │ │ │ │
│ │ Parse payload │ │ │ │
│ │ │ │ │ │
│ │──GET /internal/──→│ │ │ │
│ │ waba/{id}/tenant │ │ │ │
│ │←─{ tenant_id }───│ │ │ │
│ │ │ │ │ │
│ │──Publish Event──────────────────→│ │ │
│ │ topic: whatsapp.│ │ │ │
│ │ message.status │ │ │ │
│ │ │ │ │ │
│←──── 200 OK ─────────│ │ │ │ │
│ │ │ │──Consume Event──────→│ │
│ │ │ │ │ │
│ │ │ │ │──UPDATE ────→│
│ │ │ │ │ msg status │
│ │ │ │ │←─── OK ──────│
│ │ │ │ │ │
8.5 TRACE 4: Broadcast Message Sending Pipeline
The complete flow from campaign creation through WhatsApp delivery, covering multiple async stages.
| Step | Description |
|---|---|
| Step 1 | Create campaign: User sends POST /api/v1/broadcast/campaign with template, recipients group, and schedule. The BroadcastHandler creates a BroadcastCampaign record. If no approval is required and schedule is "immediately", a BCJob is dispatched to the in-process worker pool via dispatcher.TryDispatch().apps/broadcast-service/internal/handler/broadcast_handler.go:80-143 |
| Step 2 | Upload recipients: User uploads CSV via POST /api/v1/broadcast/recipient-groups/upload. The handler uploads the file to S3, creates an upload job record, then publishes a recipient.upload.requested event to Kafka. The broadcast worker picks up this event and processes the CSV asynchronously: validates phone numbers, creates recipient records in batches. |
| Step 3 | Trigger broadcast: If the campaign needs approval, the approver calls PATCH /api/v1/broadcast/approval/{id}. Once approved (or immediately if no approval required), the BroadcastMessageService generates individual BroadcastMessage records — one per recipient — and publishes each as a Kafka event to broadcasts.v1. |
| Step 4 | Worker consumes messages: The broadcast worker consumes broadcast.message.requested events from Kafka using a semaphore-bounded goroutine pool (default 20 workers). Each message goes through a RetryableHandler with exponential backoff. A per-tenant rate limiter (default 70 RPS, burst 10) prevents exceeding Meta API limits.apps/broadcast-service/cmd/worker/main.go:190-252 |
| Step 5 | Meta API call: The ConsumerMessageService fetches Meta credentials for the tenant (via admin-service), constructs the WhatsApp template message payload, and calls the Meta Cloud API. On success, the message status is set to "sent". On failure, error details are stored. |
| Step 6 | Webhook status updates: Meta sends delivery receipts (sent → delivered → read) via webhooks. These flow through Trace 3 above. The broadcast worker updates each message's status progressively. |
Broadcast Pipeline Sequence Diagram
Client broadcast-svc Kafka broadcast-worker admin-svc Meta API meta-webhook
│ │ │ │ │ │ │
│──POST /campaign→│ │ │ │ │ │
│←──campaign JSON─│ │ │ │ │ │
│ │ │ │ │ │ │
│──POST /upload──→│ │ │ │ │ │
│ │──Upload S3───│ │ │ │ │
│ │──Publish ───→│ │ │ │ │
│←──job_id───────│ upload.req │ │ │ │ │
│ │ │──Consume────────→│ │ │ │
│ │ │ │──Process CSV───│ │ │
│ │ │ │ (validate, │ │ │
│ │ │ │ batch insert)│ │ │
│ │ │ │ │ │ │
│──PATCH /approve→│ │ │ │ │ │
│ │──Publish ───→│ per-recipient │ │ │ │
│←──approved─────│ msg events │ │ │ │ │
│ │ │ │ │ │ │
│ │ │──Consume────────→│ │ │ │
│ │ │ (20 workers, │ │ │ │
│ │ │ rate limited) │ │ │ │
│ │ │ │──GET creds────→│ │ │
│ │ │ │←──meta token───│ │ │
│ │ │ │──POST message─────────────→│ │
│ │ │ │←──wamid.xxx────────────────│ │
│ │ │ │ │ │ │
│ │ │ │ │ │──webhook────→│
│ │ │ │ │ │ (delivered) │
│ │ │ │ │ │ │
│ │ │ ←──status event──────────────────────────────│ │
│ │ │──Consume────────→│ │ │ │
│ │ │ │──UPDATE status─│ │ │
│ │ │ │ │ │ │
8.6 Middleware Execution Order
Each service applies its own middleware chain. Middleware wraps handlers, so the last applied middleware executes first (outermost). Below is the exact execution order for each service.
gateway-service Middleware Chain
Applied in apps/gateway-service/cmd/server/main.go:130-134:
| Order | Middleware | Description |
|---|---|---|
| 1 | tracing.Middleware | Creates OpenTelemetry span, propagates trace context |
| 2 | TenantIdentifierMiddleware | Extracts subdomain from Host header, sets X-Subdomain |
| 3 | RequestIDMiddleware | Generates UUID, sets X-Request-ID header |
| 4 | CORSMiddleware | Sets Access-Control-* CORS headers |
| 5 | Router.ServeHTTP | Route matching, optional JWT validation, reverse proxy |
auth_required: true, the Router additionally validates the JWT, extracts claims, and injects X-Tenant-ID, X-User-ID, X-User-Role, and X-Subdomain headers before proxying. Role-based access control is also checked at this level.
auth-service Middleware Chain
Applied in apps/auth-service/cmd/server/main.go:126-184:
| Order | Middleware | Description |
|---|---|---|
| 1 | tracing.Middleware | Wraps entire Chi router (applied at server level) |
| 2 | chimiddleware.Logger | Logs request method, path, duration |
| 3 | middleware.RequestIDMiddleware | Ensures X-Request-ID is present |
For authenticated routes (inside r.Group), two additional middleware layers apply:
| Order | Middleware | Description |
|---|---|---|
| 4 | auth.JWTMiddleware | Validates JWT signature, expiry, Redis version checks |
| 5 | auth.TenantContextFromClaimsMiddleware | Extracts TenantContext from JWT claims into context |
admin-service Middleware Chain
Applied in apps/admin-service/cmd/server/main.go:120-217:
| Order | Middleware | Description |
|---|---|---|
| 1 | tracing.Middleware | Wraps entire Chi router (applied at server level) |
| 2 | chimiddleware.Logger | Request logging |
| 3 | middleware.RequestIDMiddleware | Request ID propagation |
For /api/v1 routes (except OAuth callback):
| Order | Middleware | Description |
|---|---|---|
| 4 | auth.TenantContextMiddleware | Reads X-Tenant-ID, X-User-ID, X-User-Role headers (set by gateway) |
For /api/v1/admin routes:
| Order | Middleware | Description |
|---|---|---|
| 5 | auth.RequireRole("Platform Admin") | Rejects non-admin requests with 403 |
broadcast-service Middleware Chain
Applied in apps/broadcast-service/cmd/server/main.go:144-171:
| Order | Middleware | Description |
|---|---|---|
| 1 | chimiddleware.Logger | Request logging |
| 2 | chimiddleware.RequestID | Request ID (chi built-in) |
| 3 | chimiddleware.Recoverer | Panic recovery, returns 500 |
| 4 | tracing.Middleware | OpenTelemetry tracing |
| 5 | auth.HybridAuthMiddleware | Accepts either JWT Bearer token OR X-Tenant-ID/X-User-ID headers (from gateway). Builds TenantContext from either source. |
meta-webhook-service Middleware Chain
Applied in apps/meta-webhook-service/cmd/server/main.go:82-86:
| Order | Middleware | Description |
|---|---|---|
| 1 | chimiddleware.Logger | Request logging |
| 2 | middleware.RequestIDMiddleware | Request ID propagation |
| 3 | chimiddleware.Recoverer | Panic recovery |
| 4 | tracing.Middleware | OpenTelemetry tracing |
8.7 Complete API Endpoint Reference
All registered routes across all services, showing the method, path, authentication requirement, and description.
gateway-service Route Configuration
Defined in apps/gateway-service/config/routes.yaml. These are prefix-matched proxy routes.
| Path Prefix | Target | Auth | Roles | Notes |
|---|---|---|---|---|
/health | localhost:8080 | No | - | Gateway health check |
/api/v1/auth | localhost:8081 | No | - | Auth-service handles its own auth |
/api/v1/admin | localhost:8082 | Yes | Platform Admin | Admin-only routes |
/api/v1/tenants | localhost:8082 | Yes | - | Tenant management |
/api/v1/oauth | localhost:8082 | No | - | OAuth callbacks |
/api/v1/broadcast | localhost:8083 | Yes | - | Broadcast service |
/internal | localhost:8082 | No | - | Service-to-service (internal_only) |
auth-service :8081
Defined in apps/auth-service/cmd/server/main.go:130-181
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /internal/users/{id} | No | Internal: get user by ID |
| GET | /api/v1/auth/ | No | Service hello |
| GET | /api/v1/auth/hello | No | Hello endpoint |
| POST | /api/v1/auth/login | No | User login (email + password) |
| POST | /api/v1/auth/refresh-token | No | Refresh access token (cookie-based) |
| POST | /api/v1/auth/reset-password | No | Reset password with code |
| POST | /api/v1/auth/users/sendmail-resetpass | No | Send password reset email |
| GET | /api/v1/auth/health | No | Health check |
| GET | /api/v1/auth/mypermission | JWT | Get current user's permissions |
| POST | /api/v1/auth/users | JWT | Create user |
| GET | /api/v1/auth/users | JWT | List users for tenant |
| GET | /api/v1/auth/users/approvers/count | JWT | Count approver users |
| GET | /api/v1/auth/users/{id} | JWT | Get user by ID |
| DELETE | /api/v1/auth/users/{id} | JWT | Delete user |
| PUT | /api/v1/auth/users/{id} | JWT | Update user |
| POST | /api/v1/auth/users/change-password | JWT | Change own password |
| POST | /api/v1/auth/roles | JWT | Create role |
| POST | /api/v1/auth/roles/permission | JWT | Create role permission |
| PUT | /api/v1/auth/roles/permission/{role_id} | JWT | Upsert role permissions |
| POST | /api/v1/auth/roles/menu | JWT | Create role-menu mapping |
| GET | /api/v1/auth/roles | JWT | List roles |
| GET | /api/v1/auth/roles/{id} | JWT | Get role by ID |
| DELETE | /api/v1/auth/roles/{id} | JWT | Delete role |
| POST | /api/v1/auth/internal/setup-tenant-role | Platform Admin | Set up default role for new tenant |
| POST | /api/v1/auth/menus | JWT | Create menu |
| GET | /api/v1/auth/menus | JWT | List menus |
| GET | /api/v1/auth/menus/{id} | JWT | Get menu by ID |
admin-service :8082
Defined in apps/admin-service/cmd/server/main.go:120-214
| Method | Path | Auth | Description |
|---|---|---|---|
| Internal (service-to-service) | |||
| GET | /internal/tenants/{id}/meta-credentials | No | Get Meta credentials for tenant |
| GET | /internal/waba/meta-credentials | No | Get all Meta credentials |
| GET | /internal/waba/{waba_id}/tenant | No | Resolve WABA ID to tenant |
| GET | /internal/me/tenants | No | Get tenant data (service-to-service) |
| GET | /internal/me/tenants/{id} | No | Get specific tenant data |
| GET | /internal/me/business-number/{phone}/{tenant_id} | No | Get business number by phone |
| Public | |||
| GET | /api/v1/oauth/meta/callback | No | Meta OAuth callback |
| Tenant Management (auth via gateway headers) | |||
| POST | /api/v1/tenants | Headers | Create tenant |
| GET | /api/v1/tenants | Headers | List tenants |
| PUT | /api/v1/tenants/{id} | Headers | Update tenant |
| GET | /api/v1/tenants/{id} | Headers | Get tenant by ID |
| DELETE | /api/v1/tenants/{id} | Headers | Delete tenant |
| POST | /api/v1/tenants/create-upload | Headers | Create upload for tenant |
| GET | /api/v1/tenants/me/meta/oauth-url | Headers | Generate Meta OAuth URL |
| GET | /api/v1/tenants/me/meta/status | Headers | Get Meta connection status |
| PUT | /api/v1/tenants/me/meta/primary-phone | Headers | Set primary WhatsApp phone |
| DELETE | /api/v1/tenants/me/meta/disconnect | Headers | Disconnect WhatsApp |
| GET | /api/v1/tenants/me/business-number | Headers | List business numbers |
| GET | /api/v1/tenants/me/business-number/my | Headers | Get my business numbers |
| POST | /api/v1/tenants/me/business-number | Headers | Create business number |
| PUT | /api/v1/tenants/me/business-number/{id} | Headers | Update business number |
| GET | /api/v1/tenants/me/business-number/{id} | Headers | Get business number by ID |
| DELETE | /api/v1/tenants/me/business-number/{id} | Headers | Delete business number |
| GET | /api/v1/tenants/me/settings | Headers | Get tenant settings |
| PATCH | /api/v1/tenants/me/settings | Headers | Update tenant settings |
| Admin-Only (Platform Admin role required) | |||
| GET | /api/v1/admin/tenants/{id}/meta-settings | Platform Admin | Get tenant Meta settings |
| PUT | /api/v1/admin/tenants/{id}/meta-settings | Platform Admin | Update Meta settings |
| POST | /api/v1/admin/tenants/{id}/meta-settings/test | Platform Admin | Test Meta connection |
| DELETE | /api/v1/admin/tenants/{id}/meta-settings | Platform Admin | Delete Meta settings |
| POST | /api/v1/admin/tenants/{id}/meta-settings/refresh-token | Platform Admin | Refresh Meta token |
| POST | /api/v1/admin/tenants/{id}/meta-settings/sync-numbers | Platform Admin | Sync phone numbers from Meta |
| GET | /api/v1/admin/tenants/{id}/meta-status/refresh | Platform Admin | Refresh Meta status |
| GET | /api/v1/admin/tenants/{id}/business-numbers | Platform Admin | Admin: list business numbers |
| POST | /api/v1/admin/tenants/{id}/business-numbers | Platform Admin | Admin: create business number |
| PUT | /api/v1/admin/tenants/{id}/business-numbers/{bn_id} | Platform Admin | Admin: update business number |
| GET | /api/v1/admin/tenants/{id}/business-numbers/{bn_id} | Platform Admin | Admin: get business number |
| DELETE | /api/v1/admin/tenants/{id}/business-numbers/{bn_id} | Platform Admin | Admin: delete business number |
| GET | /api/v1/admin/alerts | Platform Admin | Get active platform alerts |
| PUT | /api/v1/admin/alerts/{id}/acknowledge | Platform Admin | Acknowledge alert |
| PUT | /api/v1/admin/alerts/{id}/resolve | Platform Admin | Resolve alert |
| POST | /api/v1/admin/alerts | Platform Admin | Create alert |
broadcast-service :8083
Defined in apps/broadcast-service/cmd/server/main.go and handler files. Uses Huma API framework.
| Method | Path | Auth | Description |
|---|---|---|---|
| Broadcast Campaigns | |||
| POST | /api/v1/broadcast/campaign | Hybrid | Create broadcast campaign |
| GET | /api/v1/broadcast/campaign | Hybrid | List campaigns (paginated) |
| GET | /api/v1/broadcast/campaign/{id} | Hybrid | Get campaign by ID |
| PUT | /api/v1/broadcast/campaign/{id} | Hybrid | Update campaign |
| DELETE | /api/v1/broadcast/campaign/{id} | Hybrid | Delete campaign |
| POST | /api/v1/broadcast/campaign-send/{id} | Hybrid | Send/trigger broadcast campaign |
| PATCH | /api/v1/broadcast/approval/{id} | Hybrid | Approve or reject campaign |
| Broadcast Reports | |||
| GET | /api/v1/broadcast/campaign/{id}/report | Hybrid | Get campaign delivery report |
| GET | /api/v1/broadcast/campaign/{id}/messages | Hybrid | List campaign messages (paginated) |
| GET | /api/v1/broadcast/campaign/{id}/report/download | Hybrid | Download campaign report as CSV |
| GET | /api/v1/broadcast/messages/{id} | Hybrid | Send one test message via Meta |
| Recipient Groups | |||
| POST | /api/v1/broadcast/recipient-groups | Hybrid | Create recipient group |
| GET | /api/v1/broadcast/recipient-groups | Hybrid | List recipient groups |
| GET | /api/v1/broadcast/recipient-groups/{id} | Hybrid | Get group details |
| PATCH | /api/v1/broadcast/recipient-groups/{id} | Hybrid | Update group |
| DELETE | /api/v1/broadcast/recipient-groups/{id} | Hybrid | Delete group |
| GET | /api/v1/broadcast/recipient-groups/{id}/export | Hybrid | Export recipients as CSV |
| GET | /api/v1/broadcast/recipient-groups/variable/{groupId} | Hybrid | Get group variable mappings |
| Recipients | |||
| GET | /api/v1/broadcast/recipient-groups/{id}/recipients | Hybrid | List recipients in group |
| POST | /api/v1/broadcast/recipient-groups/{group_id}/recipients | Hybrid | Add recipient to group |
| GET | /api/v1/broadcast/recipient-groups/{groupId}/recipients/{id} | Hybrid | Get recipient details |
| PATCH | /api/v1/broadcast/recipient-groups/{groupId}/recipients/{id} | Hybrid | Update recipient |
| DELETE | /api/v1/broadcast/recipient-groups/{groupId}/recipients/{id} | Hybrid | Delete recipient |
| Upload | |||
| POST | /api/v1/broadcast/recipient-groups/upload | Hybrid | Upload recipients CSV (async) |
| GET | /api/v1/broadcast/recipient-groups/upload-template | Hybrid | Download CSV template |
| GET | /api/v1/broadcast/recipient-groups/{groupId}/upload-status/{jobId} | Hybrid | Get upload job status |
| WhatsApp Templates | |||
| GET | /api/v1/broadcast/whatsapp-templates | Hybrid | List templates (filterable) |
| GET | /api/v1/broadcast/whatsapp-templates/{id} | Hybrid | Get template by ID |
| POST | /api/v1/broadcast/whatsapp-templates | Hybrid | Create template |
| DELETE | /api/v1/broadcast/whatsapp-templates/{id} | Hybrid | Delete template |
| POST | /api/v1/broadcast/whatsapp-templates/{id}/duplicate | Hybrid | Duplicate template |
| POST | /api/v1/broadcast/whatsapp-templates/{id}/submit | Hybrid | Submit template for Meta review |
| POST | /api/v1/broadcast/whatsapp-templates/media/upload | Hybrid | Upload template media to S3 |
| GET | /api/v1/broadcast/whatsapp-templates/meta-example/{id} | Hybrid | Get Meta format template |
| GET | /api/v1/broadcast/whatsapp-templates/meta-media/{tenant_id} | Hybrid | Get Meta media ID |
| GET | /api/v1/broadcast/whatsapp-templates/sync-quality/{tenant_id} | Hybrid | Sync template quality from Meta |
| Broadcast Labels | |||
| GET | /api/v1/broadcast/labels | Hybrid | List broadcast labels |
| POST | /api/v1/broadcast/labels | Hybrid | Create broadcast label |
meta-webhook-service :8084
Defined in apps/meta-webhook-service/cmd/server/main.go:82-111
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/v1/webhooks/meta | HMAC | Meta webhook verification (challenge-response) |
| POST | /api/v1/webhooks/meta | HMAC | Receive Meta webhook events (signature-verified) |
| GET | /health | No | Health check |
No = No authentication.
JWT = Bearer token validated by auth-service JWTMiddleware.
Headers = X-Tenant-ID/X-User-ID headers injected by gateway.
Hybrid = Accepts either JWT token or gateway-injected headers.
Platform Admin = JWT + Platform Admin role required.
HMAC = HMAC-SHA256 signature verification with Meta app secret.
1Engage Multitenant Backend — Codebase Guide — Built for developers who need to understand the codebase quickly.