Last Updated: 2026-05-14 Status: Active Service:
apps/gateway
Complete reference for every route the Gateway exposes. All routes are mounted under https://api.vlozi.app in production and http://localhost:8788 in local dev.
1. How Proxying Works
The Gateway does not re-encode or buffer request bodies. Every proxy route follows the same three-step pattern:
// 1. Build downstream headers (strips route prefix, injects identity)
const { headers, targetUrl } = buildDownstreamHeaders(c, "/prefix")
// 2. Forward via Service Binding (zero network overhead)
const response = await proxyFetch(SERVICE_BINDING, targetUrl, method, headers, body)
// 3. Stream response back to client
const [body, status, responseHeaders] = proxyResponseArgs(response)
return c.newResponse(body, status, responseHeaders)The body is a ReadableStream passed through without buffering — SSE streams, large uploads, and chunked responses all work transparently.
1.1 Path Stripping
The Gateway mounts each proxy at a prefix (e.g. /blog) and strips that prefix before forwarding. The strip is anchored to the start of the path:
Gateway receives: /blog/admin/posts
Stripped prefix: /blog
Forwarded as: /admin/postsThis is done by buildDownstreamHeaders() which uses path.startsWith(prefix) ? path.slice(prefix.length) : path.
1.2 Identity Headers Injected
All proxy requests that pass auth carry these headers to the downstream service:
| Header | Value | Source |
|---|---|---|
x-gateway-key |
GATEWAY_SECRET env var |
Shared secret; downstream workers reject requests without it |
x-request-id |
crypto.randomUUID() |
Generated per request in auth middleware |
x-tenant-id |
ctx.auth.tenant_id |
From JWT payload or API-key record |
x-user-id |
ctx.auth.user_id |
From JWT payload; "system" for API-key requests |
x-user-permissions |
JSON.stringify(ctx.auth.permissions) |
PBAC permission strings array |
x-user-services |
JSON.stringify(ctx.auth.services) |
Service entitlement map |
The host header is explicitly removed to prevent downstream services from rejecting requests due to mismatched host expectations.
2. Blog Proxy
File: src/routes/blog.proxy.ts
Mounted at: /blog
Downstream: BLOG_SERVICE (logicspike-blog-service)
2.1 Admin Door — /blog/admin/*
Requires: JWT → PBAC blog → Subscription active
The seller dashboard uses this door for all CMS operations: creating posts, managing tags, publishing, analytics.
# Example: list posts
GET /blog/admin/posts
Authorization: Bearer <jwt>2.2 Public Door — /blog/public/*
Requires: API key (publishable or secret, blog:* permission)
Used by the @vlozi/blog SDK. The CORS policy for /blog/public/* is open (*), so any third-party website can call it from a browser with a publishable key.
# Example: fetch published posts for a tenant's site
GET /blog/public/posts?slug=my-post
x-api-key: pk_live_xxxxxPublishable keys are enforced read-only (GET/HEAD only) with optional domain locking.
2.3 Debug / Test Endpoints (DEBUG mode only)
| Path | Purpose |
|---|---|
GET /blog/debug |
Tests Service Binding connectivity to the blog worker |
GET /blog/admin/test-log |
Triggers a test log entry |
These return 404 when DEBUG !== "true".
3. Media Proxy
File: src/routes/media.proxy.ts
Mounted at: /media
Downstream: MEDIA_SERVICE (logicspike-media)
3.1 Protected Paths
| Path pattern | Auth required | Sub guard |
|---|---|---|
/media/upload/* |
JWT | ✓ |
/media/files/* |
JWT | ✓ |
Everything else (e.g. /media/health) |
— | — |
File uploads and file management are paid features. Public media delivery (if any) bypasses auth.
# Example: upload an image
POST /media/upload/image
Authorization: Bearer <jwt>
Content-Type: multipart/form-data4. Manager Proxy
File: src/routes/manager.proxy.ts
Mounted at: /manager
Downstream: MANAGER_SERVICE (logicspike-manager)
The manager handles auth, billing, team management, and workspace settings. It uses a public-path exception model — most routes require a JWT, but specific paths are explicitly whitelisted as public.
4.1 Public Paths (no auth required)
These paths are matched against the path relative to /manager using strict equality (Set.has), not prefix matching. This prevents bypasses like /manager/admin/login accidentally matching /login.
| Path | Purpose |
|---|---|
/register |
User registration |
/login |
Email/password login |
/auth/register |
Auth-flow registration |
/auth/login |
Auth-flow login |
/login/phone |
Phone login initiation |
/login/phone/verify |
Phone OTP verification |
/auth/oauth |
OAuth callback |
/auth/refresh |
JWT refresh (validates old token internally) |
/auth/verify-email |
Email ownership verification (pre-tenant) |
/auth/verify-email/resend |
Resend email verification OTP |
/auth/forgot-password |
Request password reset code |
/auth/reset-password |
Submit reset code + new password |
/onboarding/complete |
Post-registration onboarding (no JWT yet) |
/billing/webhook |
Razorpay webhook (HMAC-validated inside manager) |
/billing/plans |
Public pricing page data |
/permissions/catalog |
PBAC permission catalog |
/feedback |
Public feedback submission |
4.2 Dynamic Public Paths
These are matched by regex (not exact string):
| Pattern | Purpose |
|---|---|
GET /invitations/:token |
Team invitation landing page |
4.3 Protected Paths
Everything else requires a JWT. The manager does not apply accessMiddleware or subscriptionGuard — tenants must always reach the billing UI to reactivate.
# Example: update workspace settings
PATCH /manager/workspace/settings
Authorization: Bearer <jwt>5. Content Proxy
File: src/routes/content.proxy.ts
Mounted at: /content
Downstream: CONTENT_ENGINE_SERVICE (logicspike-content-engine)
Auth: JWT → PBAC content → Subscription active (all paths)
The content engine manages social media scheduling, AI content generation, and publish queues. All routes are protected.
GET /content/slots
Authorization: Bearer <jwt>6. Brain Proxy
File: src/routes/brain.proxy.ts
Mounted at: /brain
Downstream: BRAIN_SERVICE (logicspike-brain-service)
Auth: JWT → PBAC brain → Subscription active (all paths)
The brain service handles AI chat, memory, and agent orchestration. Responses may be SSE streams — the proxy passes the ReadableStream body through unchanged.
# Example: start an AI chat session (SSE stream)
POST /brain/chat
Authorization: Bearer <jwt>
Content-Type: application/json
{ "message": "..." }7. Contacts Proxy
File: src/routes/contacts.proxy.ts
Mounted at: /contacts-intel
Downstream: CI_SERVICE (logicspike-contact-intelligence)
Auth: JWT → PBAC contacts → Subscription active (all paths)
NOTE
The gateway route prefix is /contacts-intel but the path is stripped to / before forwarding, so the contact intelligence service receives paths without the prefix (e.g. /contacts-intel/contacts/123 → /contacts/123).
GET /contacts-intel/contacts
Authorization: Bearer <jwt>8. Newsletter Proxy
File: src/routes/newsletter.proxy.ts
Mounted at: /newsletter
Downstream: NL_SERVICE (logicspike-newsletter)
This proxy has the most complex auth structure because it serves both the seller dashboard and third-party subscriber embeds.
8.1 Public Routes
| Path | Auth | Notes |
|---|---|---|
POST /newsletter/public/subscribe |
API key (newsletter:*) |
Tenant resolved from key; x-tenant-id injected |
POST /newsletter/public/unsubscribe |
None at gateway | HMAC token verified inside newsletter service |
POST /newsletter/public/confirm |
None at gateway | HMAC token verified inside newsletter service |
The CORS policy for /newsletter/public/* is open (*) in index.ts — any website can embed a subscribe form.
# Example: subscribe a visitor
POST /newsletter/public/subscribe
x-api-key: pk_live_xxxxx
Content-Type: application/json
{ "email": "visitor@example.com", "listId": "lst_abc" }8.2 Admin Routes
All other /newsletter/* paths require JWT → PBAC newsletter → Subscription active.
The admin guards use an inline path check to avoid re-running on already-matched /newsletter/public/ paths:
newsletterProxy.use("/*", async (c, next) => {
if (c.req.path.startsWith("/newsletter/public/")) return next()
return authMiddleware(c, next)
})# Example: list campaigns
GET /newsletter/campaigns
Authorization: Bearer <jwt>9. Comms Proxy
File: src/routes/comms.proxy.ts
Mounted at: /comms
Downstream: COMMS_SERVICE (logicspike-communication)
Auth: JWT → PBAC comms → Subscription active (all paths)
The comms service handles transactional email and SMS. Has an extra debug log in DEBUG mode that prints the first 8 + last 4 chars of GATEWAY_SECRET to verify the shared secret is wired correctly.
POST /comms/email/send
Authorization: Bearer <jwt>10. Chatbot Proxy
File: src/routes/chatbot.proxy.ts
Mounted at: /chatbot
Downstream: CE_SERVICE (logicspike-chat-engine)
Auth: JWT → PBAC chatbot → Subscription active (all paths)
Like the brain proxy, responses may be SSE streams and are streamed through transparently.
POST /chatbot/sessions
Authorization: Bearer <jwt>11. Global Endpoints
These are registered directly on the root Hono app in src/index.ts.
11.1 Health Check
GET /healthNo auth. Returns 200 with binding presence map. Use to verify deployment.
{
"status": "ok",
"env": {
"BLOG_SERVICE": true,
"JWT_PUBLIC_KEY": true,
"RATE_LIMIT_KV": true
}
}12. Error Response Reference
| HTTP Status | Scenario |
|---|---|
401 Missing token |
Authorization header absent on a JWT-protected route |
401 Invalid token |
JWT signature invalid, expired, or malformed |
401 API key missing |
No x-api-key, Bearer, or ?api_key= on an API-key route |
403 Invalid API key |
Key not found in DB, or status !== "active" |
403 API key expired |
expiresAt is in the past |
403 Publishable keys are read-only |
Non-GET with a publishable key |
403 Domain not allowed |
Publishable key domain lock mismatch |
403 API key has no permissions for service: X |
Key has no X:* permission |
403 API key has read-only permissions for service: X |
Write attempt with a read-only key |
402 NO_SUBSCRIPTION |
Tenant has no subscription row |
402 TRIAL_EXPIRED |
Trial ended, no payment method |
402 SUBSCRIPTION_CANCELED |
Subscription canceled |
402 PAYMENT_FAILED |
past_due > 3 days grace period |
429 Too many requests |
Rate limit exceeded (with Retry-After header) |
500 Configuration error: X binding missing |
Worker binding not wired in wrangler.toml |
502 Gateway failed to connect to X |
Service Binding fetch threw (Worker crashed or binding error) |