A retry can represent the same operation
An owner creates a task. Postgres commits the new task, but the browser loses the response. The browser cannot tell whether the request succeeded. Sending the same body again would normally create a second task.
An idempotency key lets the client identify one intended operation across attempts. The client chooses a key once, sends it with the first request, and reuses it if the result is uncertain.
POST /api/workspaces/workspace-id/projects/project-id/tasks
Idempotency-Key: task-create-9fc2
X-CSRF-Token: session-csrf-value
Content-Type: application/json
Remember the outcome
Taskboard scopes the key to the actor, workspace, and operation. A key used for task creation does not identify a checkout operation. A second workspace does not inherit the first workspace's cached result.
The server also stores a fingerprint of the validated input. For a database-only mutation, the task change and remembered result can commit in the same transaction. On a matching retry, the server returns the recorded result rather than inserting another task.
The expected observation is one task ID across both responses and one task row in Postgres. Concurrent retries also need coordination. A unique constraint or transaction rule must make two first attempts compete for one key, instead of both deciding that the key is absent.
Check access before replay
An owner may lose workspace membership after the original request. A later replay must still pass current authentication and authorization. A stored result must not become a permanent way to bypass revoked access.
Idempotency needs a retention policy. Taskboard currently retains task-creation records without an automatic cleanup job. Deleting one would let a later attempt become new work, so cleanup must preserve a documented retry window. Checkout applies a separate twenty-three-hour safety rule to unresolved attempts. Idempotency also does not make external side effects atomic with Postgres. Checkout adds a stable provider key to address that separate boundary.
Follow task creation in Read api/src/tasks.ts and idempotency records in Read api/src/schema.ts.
Use the same key for retries of one intended operation. Persist its input fingerprint and outcome, and authorize every attempt before returning a replay.
Is disabling the submit button enough?
No. It reduces accidental clicks in one browser. Network retries, multiple tabs, and concurrent requests still reach the server. The server needs a durable rule for recognizing the same operation.