Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Downstream APIs

This guide explains how to register a backend service with Mycelium and control who can access it.

All service configuration lives in settings/config.toml under [api.services]. Restart the gateway after any change.


Registering a service

The simplest possible registration:

[api.services]

[[my-service]]
host = "localhost:3000"
protocol = "http"

[[my-service.path]]
group = "public"
path = "/api/*"
methods = ["GET", "POST"]

This tells Mycelium: “Any GET or POST to /api/* should be forwarded to localhost:3000.” No authentication required — anyone can reach this route.

The service name (my-service) becomes part of how you identify the service internally. It does not affect the URL path; the path comes from the path field.


Choosing a security group

The group field on each route controls what Mycelium requires before forwarding the request.

public — No authentication

Anyone can access the route. Use for health checks, public APIs, and Telegram webhooks.

[[my-service.path]]
group = "public"
path = "/health"
methods = ["GET"]

authenticated — Valid login token required

The user must be logged in. Mycelium injects their email in x-mycelium-email.

[[my-service.path]]
group = "authenticated"
path = "/profile"
methods = ["GET", "PUT"]

protected — Full identity required

The user must be logged in and have a resolved profile. Mycelium injects the full profile in x-mycelium-profile. Use this when your service needs to make fine-grained decisions (e.g. “can this user access this specific resource?”).

[[my-service.path]]
group = "protected"
path = "/dashboard/*"
methods = ["GET"]

protectedByRoles — Specific roles required

Only users with at least one of the listed roles can pass. All others get 403.

[[my-service.path]]
group = { protectedByRoles = [{ slug = "admin" }, { slug = "super-admin" }] }
path = "/admin/*"
methods = ["ALL"]

You can also require a specific permission level:

[[my-service.path]]
group = { protectedByRoles = [{ slug = "editor", permission = "write" }] }
path = "/content/edit/*"
methods = ["POST", "PUT", "DELETE"]

[[my-service.path]]
group = { protectedByRoles = [{ slug = "viewer", permission = "read" }] }
path = "/content/view/*"
methods = ["GET"]

Multiple routes on one service

Each [[service-name.path]] block adds a route. Mix security groups freely:

[[user-service]]
host = "users.internal:4000"
protocol = "http"

[[user-service.path]]
group = "authenticated"
path = "/users/me"
methods = ["GET", "PUT"]

[[user-service.path]]
group = "protected"
path = "/users/preferences"
methods = ["GET", "POST"]

[[user-service.path]]
group = { protectedByRoles = [{ slug = "admin" }] }
path = "/users/admin/*"
methods = ["ALL"]

Authenticating Mycelium to your service (secrets)

If your downstream service requires a token or API key from the caller, define a secret and reference it on the route.

Query parameter:

[[legacy-api]]
host = "legacy.internal:8080"
protocol = "http"

[[legacy-api.secret]]
name = "api-key"
queryParameter = { name = "token", token = { env = "LEGACY_API_KEY" } }

[[legacy-api.path]]
group = "public"
path = "/legacy/*"
methods = ["GET"]
secretName = "api-key"

Authorization header:

[[protected-api.secret]]
name = "bearer-token"
authorizationHeader = { name = "Authorization", prefix = "Bearer ", token = { vault = { path = "myc/services/api", key = "token" } } }

Load balancing across multiple hosts

[[api-service]]
hosts = ["api-01.example.com:8080", "api-02.example.com:8080"]
protocol = "https"

[[api-service.path]]
group = "protected"
path = "/api/*"
methods = ["ALL"]

Webhook routes — identity from request body

Some callers (like Telegram) don’t send a JWT. Instead, the user’s identity is in the request body. Use identitySource to handle this.

Requires allowedSources — before parsing the body, Mycelium checks that the Host header matches an allowed source. This prevents attackers from forging webhook calls.

[[telegram-bot]]
host = "bot-service:3000"
protocol = "http"
allowedSources = ["api.telegram.org"]

[[telegram-bot.path]]
group = "protected"
path = "/telegram/webhook"
methods = ["POST"]
identitySource = "telegram"

With this config, Mycelium extracts the Telegram user ID from the message body, looks up the linked Mycelium account, and injects x-mycelium-profile before forwarding. If the user hasn’t linked their account, Mycelium returns 401 and the message is not forwarded.

See Alternative Identity Providers for the full Telegram setup journey.


Service discovery (AI agents)

Set discoverable = true to make a service visible to AI agents and LLM-based tooling:

[[data-service]]
host = "data.internal:5000"
protocol = "http"
discoverable = true
description = "Customer data API"
openapiPath = "/api/openapi.json"
healthCheckPath = "/health"
capabilities = ["customer-search", "order-history"]
serviceType = "rest-api"

What headers does my service receive?

Security groupx-mycelium-emailx-mycelium-profile
authenticatedYesNo
protectedYesYes
protectedByRolesYesYes

The x-mycelium-profile value is a Base64-encoded, ZSTD-compressed JSON object. Use the Python SDK or decode it manually to read tenant memberships, roles, and access scopes.


Complete example

[api.services]

# Public health check
[[health]]
host = "localhost:8080"
protocol = "http"

[[health.path]]
group = "public"
path = "/health"
methods = ["GET"]

# User service — mixed access levels
[[user-service]]
host = "users.internal:4000"
protocol = "http"

[[user-service.path]]
group = "authenticated"
path = "/users/me"
methods = ["GET", "PUT"]

[[user-service.path]]
group = { protectedByRoles = [{ slug = "admin" }] }
path = "/users/admin/*"
methods = ["ALL"]

# Telegram webhook
[[support-bot]]
host = "bot.internal:3000"
protocol = "http"
allowedSources = ["api.telegram.org"]

[[support-bot.path]]
group = "protected"
path = "/telegram/webhook"
methods = ["POST"]
identitySource = "telegram"

Troubleshooting

Route not matching — Check that the path has a wildcard (/api/*) if you want to match subpaths. /api/ only matches that exact path.

401 on a protected route — The JWT or connection string is missing, expired, or invalid. Check the Authorization: Bearer <token> or x-mycelium-connection-string header.

403 on a role-protected route — The user is authenticated but doesn’t have the required role. Check role assignment in the Mycelium admin panel.

Downstream unreachable — Verify host, protocol, and that the service is actually running. Use acceptInsecureRouting = true on the route if the downstream uses a self-signed TLS cert.


Reference — service-level fields

FieldRequiredDescription
hostYes (or hosts)Single downstream host with port
hostsYes (or host)Multiple hosts for load balancing
protocolYes"http" or "https"
allowedSourcesRequired when identitySource is setAllowed Host headers (supports wildcards)
discoverableNoExpose service to AI agents
descriptionNoHuman-readable description
openapiPathNoPath to OpenAPI spec
healthCheckPathNoHealth check endpoint
capabilitiesNoArray of capability tags
serviceTypeNoe.g. "rest-api"

Reference — route-level fields

FieldRequiredDescription
groupYesSecurity group (see above)
pathYesURL path pattern, supports wildcards
methodsYesHTTP methods, or ["ALL"]
secretNameNoReference to a secret defined at service level
identitySourceNoBody-based IdP. Currently: "telegram"
acceptInsecureRoutingNoAllow self-signed TLS certs on downstream