Recipe - route with JWT auth
Goal: GET /me returns the caller's user id and role when the request carries a valid Authorization: Bearer <token> header. Any other request gets a 401.
The signing secret lives in app config. The check is one jsrunner block followed by an if block. Fields and rules are in the Route reference, App config reference and Test suite reference.
1. Find the project
Call list_projects:
{}Take the id of the project. Below it is <project id>.
2. Store the secret
Ask the person for the signing secret. Do not invent one or print it back.
Call save_app_config:
{
"projectId": "<project id>",
"keyName": "JWT_SECRET",
"value": "<the secret>",
"description": "Secret that signs and checks JWTs",
"isEncrypted": true,
"encodingType": "plaintext"
}It answers { "id": 7 }. A 409 Key already exists means the key is already there. Use list_app_config and keep it.
3. Create the route
Call save_route:
{
"projectId": "<project id>",
"name": "Who am I",
"path": "/me",
"method": "GET"
}It answers { "id": "<route id>" }. A new route is on in development (devActive), so call_route can run it while you build, and off in production (active), so real callers cannot reach it yet. Production also needs a promoted version, which comes at the end.
4. Read the canvas
Call get_canvas:
{ "target": { "kind": "route", "id": "<route id>" } }You get revision, blocks and edges. Note three things: the key of the entrypoint block (entrypoint_1), the key of the response block with httpCode "200" (response_1), and the revision.
5. Build the check
Pass the revision you just read. Three new blocks make the flow:
verifyreads the header and checks the token.checkbranches on the result.deniedanswers 401 on the failure branch. The existing 200 response is the success branch.
A block text input is a literal unless it starts with js:, and js: code must return the value. See Dynamic values and js: expressions before you fill a block field.
Call edit_canvas:
{
"target": { "kind": "route", "id": "<route id>" },
"revision": 0,
"validate": true,
"ops": [
{ "op": "add_block", "ref": "verify", "type": "jsrunner",
"data": { "value": "const token = (getHeader('Authorization') || '').replace('Bearer ', '');\nconst { success, payload } = jwt.verify(token, getConfig('JWT_SECRET'));\nif (!success) return { ok: false, error: 'Invalid or missing token' };\nreturn { ok: true, userId: payload.sub, role: payload.role };" },
"connect_from": { "from": "entrypoint_1" } },
{ "op": "add_block", "ref": "check", "type": "if",
"data": { "conditions": [ { "lhs": "js:return input.ok", "rhs": true, "operator": "eq", "chain": "and" } ] },
"connect_from": { "from": "verify" } },
{ "op": "connect", "from": "check", "to": "response_1", "handle": "success" },
{ "op": "add_block", "ref": "denied", "type": "response",
"data": { "httpCode": "401" },
"connect_from": { "from": "check", "handle": "failure" } }
]
}If get_canvas showed an edge from the entrypoint to the response block, add { "op": "disconnect", "from": "entrypoint_1", "to": "response_1" } as the first op. A handle holds one edge, so the connect would be refused without it.
The answer has the new revision, refs and, with validate, any issues. Errors are refused and nothing is saved. Warnings still save.
The body of a response is the output of the block before it. The 200 answer is { "ok": true, "userId": "...", "role": "..." }. The 401 answer is { "ok": false, "error": "Invalid or missing token" }.
6. Check the logs, then switch it on
Call get_system_logs:
{ "projectId": "<project id>", "level": "error", "resourceId": "<route id>" }An empty list means it compiled. Then switch production on:
Call save_route:
{ "routeId": "<route id>", "active": true }Production serves a promoted version only. When the user wants it live, call commit_version with a title, then promote_version with the new versionId. A person approves the promote and says why.
7. Try it
No token must fail:
Call call_route:
{ "routeId": "<route id>" }Expect status: 401. Then with a token signed by the same secret:
Call call_route:
{ "routeId": "<route id>", "headers": { "Authorization": "Bearer <token>" } }Expect status: 200. Ask the person for a token. jwt.sign({ sub: "u1", role: "admin" }, secret) makes one.
8. Keep it tested
Save both cases as a test suite and run it. See Recipe, write and run a test suite. The 401 case needs no token. For the 200 case, sign a token in the suite's setup block and read it as t.setup.
Reuse the check on many routes
A guard repeated on many routes belongs in a middleware. Make a custom block with usage: "middleware" that holds the same jsrunner and if. End its failure branch in a response block with httpCode "401", which stops the request before the route runs. Chain it in save_middleware. Attach it to each route with save_route and middlewares: { before: ["<middleware id>"] }.
Common problems
| What you see | What to do |
|---|---|
call_route says Route is switched off in development | Run save_route with devActive: true. |
| Always 401, even with a good token | The secret differs. The token must be signed with the JWT_SECRET value. Header must start with Bearer . |
jwt or getConfig is not defined | The code must run in a jsrunner block or a js: field, not a plain text field. |
edit_canvas says Canvas changed since you read it | Call get_canvas again and redo the edit with the new revision. |
500 and an error in get_system_logs | Read the message. A script error names the block. Use Recipe, debug and fix a route or workflow. |
