Date: 2026-08-07
Ordered work to turn this repository into Relay. Read context.md and requirements.md first.
Phases 1 and 2 are mechanical and reversible. Phase 3 is the new capability. Phase 4 is infrastructure. Keep the test suite green throughout — it is the only thing standing between a refactor and a silent behaviour change in a service that sends real email.
Baseline, verified at the specification commit: 493 tests pass and
ruff check . is clean.
Note that the two repositories had different ruff configurations — taskdeck
enabled BLE001, this project does not — so a noqa in the merged tree became
unused and was removed to get the baseline clean. Expect a few more of that kind
as the two configurations are reconciled in Phase 1; they are cosmetic, but do
not let them accumulate, because a permanently-failing lint stops being read.
Goal: taskdeck becomes an app in this repo rather than a git dependency.
No behaviour change.
git mv taskdeck-src/taskdeck taskdeck— the Django app. Keep the app labeltaskdeck. Its migration0001_initialis already applied in datamailer's database, and renaming the label means a migration rewrite for no benefit.git mv taskdeck-src/tests tests/taskdeck— keep them; they cover the signal binding, which is subtle and easy to break.git mv taskdeck-src/docs/decisions.md docs/taskdeck-decisions.mdandtaskdeck-src/docs/contract.md docs/contract.md.taskdeck-src/docs/requirements.mdis superseded by this repo's — read it once for the reasoning, then delete.- Delete
taskdeck-src/remnants:pyproject.toml,README.md,uv.lock. - In
pyproject.toml, remove the"taskdeck"dependency and the[tool.uv.sources]entry pinning it to git. Fold in anything it declared that this project does not already have. - The
Dockerfileinstallsgitsolely to resolve that git-pinned dependency. Remove that, and theapt-get purgethat follows it. uv sync && uv run pytest. Expect green with no code edits — the import pathtaskdeck.*is unchanged.
Verify: grep -rn "taskdeck" pyproject.toml returns nothing, and
taskdeck still appears in INSTALLED_APPS.
Goal: the Django project module becomes relay. Domain apps keep their names.
git mv datamailer relay— settings, urls, wsgi, asgi.- Update every reference to
datamailer.settings/datamailer.wsgi:manage.py,pyproject.toml(DJANGO_SETTINGS_MODULEunder[tool.pytest.ini_options]),conftest.py,Dockerfile,Makefile,.github/workflows/,scripts/. TASKDECK_PROJECT = "relay"in settings — this is the identity the status contract reports and what a console keys on.[tool.ruff.lint.isort] known-first-party→["relay", "mailing", "taskdeck"].
Do not rename:
- the
mailingapp, or anydb_table. The tables carry real data and the names are explicit in the models; renaming buys nothing and risks everything. - environment variable names (
DATAMAILER_*). Rename them later, deliberately, with the deployment — not in the same change as a module move.
Verify: uv run pytest, uv run ruff check ., and
uv run python manage.py check.
Goal: the capability Relay exists for. This is design work, not mechanical. Do not start it until Phases 1 and 2 are green and committed.
Build in this order — each step is useful on its own:
One endpoint that accepts a task submission, returns an id, and one that reports status. Payload shape mirrors the status contract, so a console renders a Relay job with the code it already uses for a local task.
- Client-supplied
idempotency_keyis mandatory (R7). Reusefind_existing_message's pattern from the email path. - Scope every query by owner (R5). The API key already resolves to a client.
The mechanism that removes per-project workers. A task type whose work is an authenticated HTTP call to a client URL.
- Sign the request. HMAC over body and timestamp, per-client secret, so the receiver can verify origin and reject replays.
- Pass the task id so the receiver can deduplicate. Retries will happen.
- Bound it with a timeout, and document the ceiling. See open decision 1 in context.md — resolve it before building, since it decides whether an ack-then-callback path is needed now.
- Retry with backoff. Distinguish 4xx (client's problem — do not retry blindly) from 5xx and timeouts (do retry).
- Record the response status and a truncated body on failure. A webhook task that fails with no detail is undebuggable from the console.
Cron owned by Relay, firing a built-in or webhook task. Report last run, last success, and whether a run was missed — a schedule that stops firing looks exactly like a quiet period otherwise.
There is a working reference for the reporting half in mailing/ops_views.py
(_schedules) and TASKDECK_SCHEDULES in settings.
mailing/aws.py builds every client from ambient credentials today. Replace
with a resolver: task type → role ARN, assume, cache until shortly before
expiry, build clients from those credentials.
Assume-role failure must fail the task. It must not fall back to ambient credentials — that silently does the work with the wrong identity, which is the exact failure the scheme exists to prevent.
There is a related design already written up for the email half:
DataTalksClub/aws-infra issue #19 covers per-tenant SES sender roles.
Goal: Relay runs on its own infrastructure. Datamailer is untouched.
Raise as a pull request against DataTalksClub/aws-infra. Do not apply.
- New root, e.g.
sandbox/relay. Do not editsandbox/datamailer. eu-west-1, per the promotion rule inaws-infra/docs/state-boundaries.md. Datamailer'sus-east-1is legacy and must not be inherited by copying.- Own queues, own SES configuration set, own database, own DNS name. Nothing shared with datamailer — a shared resource is a shared outage.
- Keep the two AWS-fed ingress queues (
ses-webhooks,inbound-email); their producers are SNS and S3, so they cannot move onto the internal queue. - An Elastic IP if the compute keeps a public address. Datamailer's does not have one, so a stop/start changes its IP and breaks DNS — do not repeat that.
- The worker must be in the deployment definition, and the deploy must fail if it is not running (R2).
Cost: the owner is explicit about keeping this low. Postgres in a container on
the app host costs nothing beyond the instance; RDS db.t4g.micro adds roughly
$12/month plus storage. Do not reach for RDS without asking.
Only after Relay is deployed and proven.
- DataTalksClub website — first client using both halves. Design the job API against its real needs rather than guesses.
- CMP — migrate from datamailer to Relay for email. This is a send path, so it moves gradually and reversibly, never as a cutover. Both point at the same SES identity, so a client can be switched by changing one URL.
- AISL — general background work. Largest win, largest migration; not a first-release requirement.
Datamailer is retired only when nothing points at it, and not before.
uv, notpip.- Keep the suite green. If a test asserts old behaviour, change it deliberately and say why in the commit — do not delete it to get to green.
- Do not push to
DataTalksClub/datamailer. Itsmainauto-deploys to the host serving CMP production email. - Infrastructure by pull request.
- Read the four mistakes in context.md before building the worker or the task layer. They are all cheap to repeat.