Skip to content

docs: add API v1 contract (docs/api.md) - #8

Merged
devinder-dev merged 1 commit into
mainfrom
docs/api-contract
Oct 26, 2026
Merged

devinder-dev merged 1 commit into
mainfrom
docs/api-contract

Conversation

@devinder-dev

Copy link
Copy Markdown
Owner

Adds docs/api.md: the API v1 contract with every request and response shape, so the API and the dashboard build against one file instead of two descriptions that drift apart. This moves the planned "API contract" step earlier, because the dashboard needs it now.

What's in it

  • Conventions: auth, JSON only, 256 KiB limit, UUIDv7 ids, ISO times, X-Request-Id, 404 for other owners' ids, CORS.
  • Errors: the one error shape, details on 422, and the full list of status codes and code values.
  • Cursor pagination: { data, next_cursor }, newest first.
  • TypeScript types the dashboard can copy into src/types.ts.
  • Every route with its body rules, answer and errors.

Adopted from PR #7

  • Nested event and endpoint objects in delivery rows, instead of flat event_type / endpoint_name. It matches the detail answer and reads better in the dashboard. The API maps database rows to this shape in its service layer.
  • attempt_log for the history of tries. The earlier draft used attempts for both the counter and the list.

Clarified

  • Replay resets attempts to 0. The history (attempt_log) is never deleted, and tries keep their numbers.
  • Endpoints have a status (active / failing / disabled), worked out when the list is read.
  • GET /v1/me returns the owner and the key prefix.
  • Attempt errors are codes (timeout, connect_failed, blocked_address, redirect, tls); null when the receiver answered with a status code.
  • next_attempt_at is null unless the delivery is pending.

docs/CONTEXT.md now points to the new file.

How to review

Read docs/api.md top to bottom and check that the dashboard screens can be built from these shapes alone. Once merged, PR #7 can align dashboard/src/types.ts to it.

One file with every request and response shape, followed by both the API and the dashboard. Adopts the nested event/endpoint objects and the attempt_log name proposed in PR #7, and clarifies replay, endpoint status, the /v1/me answer and attempt error codes.

@AbdikhaniAbdirashid AbdikhaniAbdirashid left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Read it against all four screens - they can be built from these shapes alone. The attempts vs attempt_log section settles what I got wrong in #7.

Three questions, none blocking:

  1. CORS. The table says cdn.myrelay.tech "and localhost in CI". Does that mean localhost:5173 cannot call the real API while I develop? If so I need to run the API locally once I move off mock data - better to know now than later.

  2. in_flight. Replay can answer 409 in_flight, but no field on Delivery says a worker is sending right now, so the Replay button cannot be greyed out up front. It has to fire, get the 409 and show the error. Fine by me, just confirming that is intended.

  3. disabled is unreachable. active and failing are derived from deliveries, but disabled needs someone to choose it, and there is no PATCH and no DELETE. So one of the three values can never occur in v1.

My suggestion: drop disabled from EndpointStatus for now and keep it with DELETE under "later". No screen needs it yet, failing already tells me a receiver is down, and the circuit breaker you listed is the real answer anyway.

One thing that may matter sooner: with a hard limit of 20 endpoints and no DELETE, a few typos while testing burn slots permanently. Is 20 generous enough to live with before the defense?

@devinder-dev

Copy link
Copy Markdown
Owner Author

Thanks for checking it against all four screens. Good questions, and all three are now in #10:

  1. CORS [...] Does that mean localhost:5173 cannot call the real API while I develop?

Right, the real API only accepts browser calls from https://cdn.myrelay.tech and http://localhost:4173 (Playwright's preview server in CI). But you don't need to run the API locally: add a dev proxy in vite.config.ts:

server: { proxy: { "/v1": { target: "https://api.myrelay.tech", changeOrigin: true } } }

and leave VITE_API_URL empty in development, so axios calls /v1/... on localhost:5173. The browser then only talks to your dev server, so CORS never applies, and the API's CORS can stay strict. The production build sets VITE_API_URL=https://api.myrelay.tech.

  1. in_flight [...] It has to fire, get the 409 and show the error. Fine by me, just confirming that is intended.

Yes, intended. A send takes at most a few seconds, so it's rare. On 409 in_flight show "being sent right now, try again in a moment". If it gets in the way during the demo, we can add a field later as an additive change.

  1. disabled is unreachable [...] with a hard limit of 20 endpoints and no DELETE, a few typos while testing burn slots permanently.

Agreed on both. #10 adds DELETE /v1/endpoints/:id to v1 as a soft delete: the endpoint leaves the list and frees its slot, and its old deliveries keep its name and URL. EndpointStatus is now active | failing. Creating a 21st endpoint answers 409 endpoint_limit_reached.

devinder-dev added a commit that referenced this pull request Oct 29, 2026
Follow-up to #8, answering the three review questions there.

## Changes to `docs/api.md`
- **`DELETE /v1/endpoints/:id` is now part of v1.** It's a soft delete:
the endpoint disappears from the list and frees its slot, but its old
deliveries still show its name and URL. Deliveries still pending for it
become `dead` with `last_error: "endpoint_deleted"`. Without it, test
typos would use up the 20 slots for good.
- **`EndpointStatus` is `active | failing`.** `disabled` could never
occur in v1. Deleted endpoints simply aren't listed.
- **New error codes:**
  - `409 endpoint_limit_reached` when creating a 21st endpoint;
- `409 endpoint_deleted` when replaying a delivery whose endpoint was
deleted.
- **Replay:** the dashboard can't know in advance that a delivery is
being sent right now, so the button tries, and a `409 in_flight` is
shown as "being sent right now, try again in a moment".
- **CORS and local development:** browsers may call the API only from
`https://cdn.myrelay.tech` and `http://localhost:4173` (Playwright's
preview server in CI). For local development, the Vite dev server
proxies `/v1` to the API, so CORS never applies. The config snippet is
in the doc.

`docs/CONTEXT.md` lists the new route.

The types block in `docs/api.md` compiles with strict TypeScript.
AbdikhaniAbdirashid added a commit that referenced this pull request Oct 31, 2026
Updated to match `docs/api.md` (#8), as requested.

- `dashboard/src/types.ts` — the types block from `docs/api.md`, copied
over. Not a proposal any more.
- `dashboard/src/mocks.ts` — fake data shaped to those types, for
building the four screens before the API exists.

## Your review points

| # | Done |
|---|---|
| 1 | Tool attribution gone from the commit and from this description |
| 2 | Replay resets `attempts` to 0; `attempt_log` keeps its eight rows
|
| 3 | `status: active \| failing` on endpoints; `GET /v1/endpoints`
returns `{ data }` with no paging |
| 4 | `me` is `{ owner: { id, name }, key: { prefix } }` |
| 5 | Attempt errors are `AttemptError` codes, `null` when the receiver
answered |
| 6 | `ApiError.details` added, with 401 and both 409 bodies as fixtures
|
| 7 | Timelines add up, `example.com` URLs,
`whsec_MOCK_not_a_real_secret`, header points at `docs/api.md` |

## On the timelines

Every gap in an attempt log starts when the previous try **ended**
(`started_at + duration_ms`) and follows the retry schedule. The dead
delivery's eight tries are exact to the millisecond, and the pending
delivery's three tries total 10.350 s, which is what makes its
`next_attempt_at` land five minutes after try 3.

## What the mocks cover

| Fixture | Why |
|---|---|
| pending, 3 tries | the row the detail screen reloads every 2 s |
| dead, 8 tries | the exhausted-budget example; pending can be replayed
too |
| delivered, 1 try | the common case |
| delivered, 2 tries | `attempts > 1` can still end well |
| dead, endpoint_deleted | soft-deleted endpoint example; endpoint is
missing from the list |
| 3 endpoints | two active/failing, one shown on Create form response |
| 401, 409 ×3, 422 | Connect (401), Replay (409
in_flight/already_delivered/endpoint_deleted), Create (409
endpoint_limit_reached), Add form (422) |

Endpoint `status` is derived the way `docs/api.md` says: n8n is
`failing` because its latest delivery is pending after a failed try;
Slack is `active` because its latest went through.

## Checked

`tsc --noEmit` passes. All 27 ids are valid UUIDv7 with valid variant
bits.

## Not covered on purpose

No `EventAccepted` fixture — no screen posts events. No pagination
fixture — four rows is under the default limit of 50.

---------

Co-authored-by: Abdikhani Abdirashid <Abdikhani_abdirashd@hotmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants