A general API with personal access tokens and scopes, beyond the capture API #12

Closed
opened 2026-09-05 12:29:07 +00:00 by tiagoagueda · 0 comments
Owner

Why this exists

postulo-mcp (#19) needs to read and change a person's job hunt on their behalf. The browser extensions (#17, #18) will want to ask "is this posting already tracked?" and show the answer. The capture API is designed to be unable to do any of that — the docstring of api/api.py and the wiki page The capture API both promise it: cannot read, cannot change, cannot sign in. That design is right for a token that lives in a browser profile. It does not stretch to an agent.

What exists today

  • One NinjaAPI at /api/v1 with three routes: GET /me, POST /captures, GET /captures. docs_url=None, so there is no OpenAPI page.
  • CaptureToken (api/models.py): hashed at rest, shown once, a prefix for identification, last-used, revocable. Its docstring says the scope is not configurable "because there is nothing to configure". This issue is where that stops being true.
  • Domain logic already lives in services — applications/services.py: change_status, record_event — so an API that writes goes through the same path as the forms and the event log stays the single truth.

Shape

  1. Personal access tokens with scopes: captures (what today's token is), read, write, and documents:read on its own because files are the most sensitive thing Postulo holds. Same storage design as capture tokens — hash, prefix, shown once, last-used, revoke — plus an expiry. Migration: every existing CaptureToken becomes a token with the captures scope, so nothing anyone has installed stops working. Your details → Capture tokens becomes API tokens.
  2. Resources, all owner-scoped through for_user() exactly as the views are. Read side first: applications (list, filter, detail with timeline), companies, contacts, postings, reminders, CVs and cover letters (structured and rendered), uploads (metadata; the file itself behind documents:read), insights.
  3. Writes through services only: record an application, change status, add an event or note, add and complete reminders, create and update companies and contacts, create a cover letter draft, request a render. No deletion in the first cut.
  4. Actor on the event log. A write through the API records which token did it ("via API token laptop-agent"), so the timeline shows what an agent did and what the person did. #19 depends on this being visible.
  5. OpenAPI on (/api/docs): a machine-readable schema is what an MCP server and an extension author consume, and it is the documentation.
  6. Versioning: stays /api/v1; the capture endpoints do not move or change shape.
  7. Limits: pagination everywhere, a per-token rate limit, one error shape.
  8. Wiki: The capture API becomes The API; the capture section stays as it is, and the "what it cannot do" promise is rewritten per scope so it remains true.

Classification

Enhancement. Not breaking on one condition: existing capture tokens keep working unchanged through the migration to scoped tokens. Dropping that condition would make it a breaking change; it is not proposed.

Open questions

  1. Scope granularity: the four above, or per resource? Proposal: the four; per-resource scopes are engineering for a request nobody has made.
  2. OAuth 2.1 for third-party clients, which the remote MCP transport wants: later, if ever. Personal access tokens are what a self-hoster expects and understands.
  3. Outbound webhooks: not here — that is #4's notifier interface with an HTTP notifier.
## Why this exists postulo-mcp (#19) needs to read and change a person's job hunt on their behalf. The browser extensions (#17, #18) will want to ask "is this posting already tracked?" and show the answer. The capture API is *designed* to be unable to do any of that — the docstring of `api/api.py` and the wiki page *The capture API* both promise it: cannot read, cannot change, cannot sign in. That design is right for a token that lives in a browser profile. It does not stretch to an agent. ## What exists today - One `NinjaAPI` at `/api/v1` with three routes: `GET /me`, `POST /captures`, `GET /captures`. `docs_url=None`, so there is no OpenAPI page. - `CaptureToken` (`api/models.py`): hashed at rest, shown once, a prefix for identification, last-used, revocable. Its docstring says the scope is not configurable "because there is nothing to configure". This issue is where that stops being true. - Domain logic already lives in services — `applications/services.py`: `change_status`, `record_event` — so an API that writes goes through the same path as the forms and the event log stays the single truth. ## Shape 1. **Personal access tokens with scopes**: `captures` (what today's token is), `read`, `write`, and `documents:read` on its own because files are the most sensitive thing Postulo holds. Same storage design as capture tokens — hash, prefix, shown once, last-used, revoke — plus an **expiry**. Migration: every existing `CaptureToken` becomes a token with the `captures` scope, so nothing anyone has installed stops working. *Your details → Capture tokens* becomes *API tokens*. 2. **Resources**, all owner-scoped through `for_user()` exactly as the views are. Read side first: applications (list, filter, detail with timeline), companies, contacts, postings, reminders, CVs and cover letters (structured and rendered), uploads (metadata; the file itself behind `documents:read`), insights. 3. **Writes through services only**: record an application, change status, add an event or note, add and complete reminders, create and update companies and contacts, create a cover letter draft, request a render. No deletion in the first cut. 4. **Actor on the event log.** A write through the API records *which token* did it ("via API token *laptop-agent*"), so the timeline shows what an agent did and what the person did. #19 depends on this being visible. 5. **OpenAPI on** (`/api/docs`): a machine-readable schema is what an MCP server and an extension author consume, and it is the documentation. 6. **Versioning**: stays `/api/v1`; the capture endpoints do not move or change shape. 7. **Limits**: pagination everywhere, a per-token rate limit, one error shape. 8. **Wiki**: *The capture API* becomes *The API*; the capture section stays as it is, and the "what it cannot do" promise is rewritten per scope so it remains true. ## Classification Enhancement. Not breaking on one condition: existing capture tokens keep working unchanged through the migration to scoped tokens. Dropping that condition would make it a breaking change; it is not proposed. ## Open questions 1. Scope granularity: the four above, or per resource? Proposal: the four; per-resource scopes are engineering for a request nobody has made. 2. OAuth 2.1 for third-party clients, which the remote MCP transport wants: later, if ever. Personal access tokens are what a self-hoster expects and understands. 3. Outbound webhooks: not here — that is #4's notifier interface with an HTTP notifier.
tiagoagueda added this to the 0.2.0 milestone 2026-09-05 12:29:07 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
Postulo/postulo#12
No description provided.