Skip to content
Algoryq Learn

Developers

A REST API where every route carries a permission key

The LMS API this product's own frontend runs on — versioned, permission-checked and correlation-tagged. There is no privileged internal one.

Two people working through code together at a laptop, in an open brick-walled workspace.

The shape of the API

497

routes

31

modules

272

permission keys

135

models behind them

Authentication

API keys are hashed at rest and shown once, at creation. A lost key is rotated, not recovered — a key a vendor can show you again is a key stored in a way you should mind.

Every request carries the permissions of the key's owner. There is no “API user” with special powers: a key that can publish a course is held by somebody who can.

Send a correlation id and it travels through every log line the request touches, which is what makes a support conversation short.

curl https://api.learn.algoryq.com/v1/courses \
  -H "Authorization: Bearer $ALGORYQ_API_KEY" \
  -H "X-Correlation-Id: $(uuidgen)"

The shape of a response

Success

One envelope everywhere: data plus meta. Lists are paginated; the total is real, not an estimate.

{
  "data": [ { "id": "…", "title": "Data Structures" } ],
  "meta": { "page": 1, "pageSize": 20, "total": 43 }
}

Failure

RFC 7807 problem+json. The human-readable message lives in title, not detail — our own frontend once dropped every API error by reading the wrong field, so it is worth saying out loud.

{
  "type": "https://learn.algoryq.com/errors/forbidden",
  "title": "You do not hold course.course.publish",
  "status": 403,
  "instance": "/v1/courses/42/publish"
}

The public endpoints, and their threat model

Almost everything requires a key. Three things do not, each designed with a specific abuse in mind.

POST /public/institutes/:slug/enquiries

Web-to-lead. It forces the source and has no fields for stage, owner or pipeline — absent from the schema rather than validated away, because a field accepted and then overridden is one somebody eventually forgets to override. It returns a fixed acknowledgement, so it cannot be asked whether an address is on file, and answers identically for an unknown or suspended institute, so it cannot enumerate them. It notifies nobody automatically: an unauthenticated request that can put a message in a named person's inbox is a spam vector wearing a feature's clothes.

The form on this website posts to it.

GET /verify/:code

Certificate verification. Whoever holds the code is usually an employer, not a user. It returns what was certified, to whom, when, and whether it still stands, and nothing else. Unknown and revoked produce different words, because they mean very different things to the person checking.

GET /public/sites/:slug

An institute's own public website — pages, posts, events, gallery. Author text is Markdown rendered to React elements; the renderer never emits HTML, so nothing an author types can become script in a visitor's browser.

Webhooks

Register an endpoint, subscribe to events, and every delivery is signed with an HMAC over the raw body. Verify before you parse — parsing first is how a signature check becomes decorative.

Deliveries are logged with status and response, retried on failure, and replayable from the interface. A webhook you cannot inspect is one you cannot debug at 9pm.

Events, signatures and retries in detail

POST https://your-endpoint.example.com/algoryq-learn
X-Algoryq-Signature: sha256=<hmac of the raw body>
X-Algoryq-Delivery: 01J2…
X-Algoryq-Event: enrollment.created

Run it yourself

Three commands, no cloud account. The second line is three steps on purpose: migrations create the schema, row-level security is applied separately from the database catalogue so a table added later is covered automatically, and the seed installs the permission catalogue and role templates.

Run exactly one worker. Its jobs claim no rows, so a second copy double-sends email.

The full self-hosting guide

docker compose up -d
pnpm db:migrate && pnpm db:rls && pnpm db:seed
pnpm --filter @akechi/api worker:dev

What the API does not have

  • No GraphQL.
  • No public sandbox key — you need an instance, and the compose file gives you one.
  • No SCIM, no SAML, no OIDC.
  • No official client libraries beyond the typed internal SDK.
  • No hosted, browsable reference here yet. The OpenAPI document is exported from the running application, so it is generated from the routes rather than maintained by hand, and publishing it is on the list.

Read the security model before you build against it.

Deny-by-default authorization, forced row-level isolation and a hash-chained audit log, with the file each one lives in.

Or open the sandbox (opens in a new tab) and click around first. No form.