Internal API
How the dashboard talks to Kora's API, who a request acts as, and how permissions are checked.
Kora runs two servers. The dashboard on port 3000 serves the web app and signs members in. The API on port 3001 holds every route, including your module's. The dashboard never touches the database. It calls the API for everything.
Requests From the Dashboard
When the dashboard calls the API it sends Kora's internal secret plus headers that say who the signed-in member is:
| Header | What it carries |
|---|---|
Authorization: Bearer <secret> | Kora's internal secret from config.json. |
x-actor-kind | staff or customer. |
x-actor-id, x-actor-name | The member. |
x-actor-permissions | Their permissions as a JSON array. |
x-actor-customer | The customer ID, for client dashboard members. |
x-actor-discord | Their Discord ID, if linked. |
x-actor-ip | Their real IP address. |
The API trusts these headers only alongside the secret. Keep the secret private, since anything holding it can act as any member.
This happens in three places:
| Where | Path |
|---|---|
Your page's load() | client.send("GET", "/api/modules/<module>/<path>") |
| Resource pages and your own views | request(module, path), which goes through /api/m/<module>/<path> on the dashboard |
| Kora's own pages | Built-in routes such as /api/settings and /api/audit |
In each case the request reaches your route as that member, so you never handle sessions yourself.
Actors
Every request gets an actor on request.actor:
| Kind | From |
|---|---|
system | The secret with no actor headers, such as Kora's own startup calls. Has every permission. |
staff | A dashboard team member. |
customer | A client dashboard member. |
token | An API token from the API Tokens page. Its permissions are the token's scopes. |
public | Nobody, on a route with access: "public". Has no permissions. |
A private route with no valid secret or token answers 401. A token that does not match is written to the audit log.
Checking Permissions
Routes check permissions for you through their permissions option. To check by hand:
if (!this.kora.api.allowed(request.actor, "licenses.edit")) this.fail(403, "You cannot edit licenses.");| Actor has | Passes |
|---|---|
* | Everything. |
licenses.edit | licenses.edit. |
licenses.* | Any licenses. permission. |
licenses.manage | Any licenses. permission, for staff. |
customer is a special permission that only customers and the system pass. Public actors fail every check except an empty one.
Request Helpers
| Helper | What it does |
|---|---|
this.kora.api.allowed(actor, permission) | The check above. |
this.kora.api.ip(request) | The caller's IP. Reads x-actor-ip from the dashboard, then cf-connecting-ip, then x-forwarded-for. |
this.kora.api.address() | The public API address from API URL, or http://localhost:3001. Use it for links you give out, such as webhook URLs. |
Built-in Routes
These belong to Kora rather than a module. They all need the secret except the first three.
| Route | What it does |
|---|---|
GET /api/status | Whether Kora is running and configured. |
GET /api/health | 200 when Kora is running with the database up and no failed modules, otherwise 503. Point uptime monitors here. |
GET /api/v1/public-key | The Ed25519 public key used to sign responses. |
GET /api/modules, POST /api/modules/toggle | List modules and turn them on or off. |
GET /api/pages | The pages the member can see. |
GET /api/modules/configs, POST /api/modules/config | Read and save config groups. |
GET /api/modules/langs, POST /api/modules/lang | Read and save lang text. |
GET /api/settings, POST /api/settings | Kora's settings, with secrets hidden. |
GET /api/tokens, POST /api/tokens, DELETE /api/tokens/:id | API tokens. |
GET /api/audit | The audit log. |
GET /api/logs | Recent console lines. |
Your module's routes live under /api/modules/<module>, or at their own path when they set absolute: true. See Routes.