## Summary - Standalone Django 6 shortener: Bearer `/api/links/` (create/list/detail/disable) and public `GET /<code>` 302 - Host split, target-host allowlist, named rotatable tokens; `short_url` from `PUBLIC_SHORT_URL` - Landing page, DEBUG-only `/debug/` mint form, Django admin - Docker/compose (host **8005**), Gitea CI like monica_site (PR tests, beta on merge, prod button) - Caller contract in `API.md` Closes #1. Infra follow-up: [server-infra#22](ai_ml_operations/server-infra#22). ## Test plan - [ ] `cd site && uv run python manage.py test` - [ ] `docker compose up --build` → http://127.0.0.1:8005/ - [ ] `POST /api/links/` with `Bearer monica:dev-only-token` → 201 - [ ] `GET /<code>` → 302 to allowlisted https URL - [ ] No Bearer → 401; non-allowlisted host → 400 - [ ] `/debug/` only when `DEBUG=true` Reviewed-on: #2
url_shortening_service
Django 6 URL shortener. monica_site (and other trusted callers) mint links over
Bearer auth on an API hostname (may be public DNS). SMS recipients hit
GET /<code> on the short domain and get a 302 to the long HTTPS URL.
This service is standalone. Do not fold it into monica_site.
Two surfaces
| Who | Path | Auth | Host |
|---|---|---|---|
| Phone / public internet | GET /<code> |
none | SHORT_DOMAIN (NPM + TLS) |
| Anyone | GET / |
none | landing page on the short domain |
monica_site |
/api/links/ |
Authorization: Bearer name:secret |
own DNS / NPM host — not the short domain |
The API may be on the public internet. It is not open: every /api/ request needs a
named Bearer token. No token / wrong token → 401. No tokens configured → 503.
GET /<code> never requires a token.
/api/ and /admin/ are 404 on the short domain. /admin/ is also 404 on the
public API hostname (localhost only). GET /debug/ is a mint form when
DEBUG=true and never on the short domain.
Local run
cp .env.example .env
# SHORTENER_API_TOKENS=monica:dev-only-token is already set
docker compose up --build
Or without Docker:
uv sync
cp .env.example .env
cd site && uv run python manage.py migrate
uv run python manage.py runserver
# optional local admin:
# uv run python manage.py createsuperuser
# then http://127.0.0.1:8005/admin/ (compose) or :8000 (runserver)
Tests (SQLite, no network):
cd site && uv run python manage.py test
CI / deploy (Gitea)
Same split as monica_site:
| Workflow | When | What |
|---|---|---|
CI |
pull request → master |
unit tests |
Deploy Beta |
push / merge to master |
unit tests → compose tests → deploy beta |
Deploy Prod |
Actions → Run workflow (button) | unit tests → compose tests → deploy prod |
Deploy calls server-infra/scripts/deploy.sh --app url_shortening_service. Needs server-infra#22 first.
Use case
- Your site POSTs to this service with a Bearer token and a long HTTPS URL.
- Response
201includesshort_urlbuilt fromPUBLIC_SHORT_URL(not the API Host). - A person on the public internet opens that short URL.
- This service 302s them to the long URL and increments
click_count.
Mint (API host / localhost):
curl -sS -X POST http://127.0.0.1:8005/api/links/ \
-H "Authorization: Bearer monica:dev-only-token" \
-H "Content-Type: application/json" \
-d '{"target_url":"https://mkdrealtor.com/","title":"test"}'
Follow (no token — this is the public path):
curl -sSI http://127.0.0.1:8005/<code>
Expect HTTP/1.1 302 Found and Location: https://mkdrealtor.com/.
Caller integration (onboard + /api/links/): API.md.
Caller contract (monica_site, other repo)
SHORTENER_BASE_URL=https://shortener.aimloperations.com
SHORTENER_API_TOKEN=monica:<same-secret-as-SHORTENER_API_TOKENS>
POST /api/links/
Authorization: Bearer monica:<secret>
Content-Type: application/json
{"target_url":"https://mkdrealtor.com/listings/oak-st?utm_source=monica&utm_medium=sms","title":"Oak St","external_ref":"campaign-uuid"}
target_url must be https and its host must match SHORT_ALLOWED_HOSTS
(exact or suffix, e.g. mkdrealtor.com). Same target_url + external_ref +
still-active link returns 200 with the existing row instead of a new code.
Do not call the public short hostname to create links.
Environment
See .env.example and .env.prod.example. Prod secrets live in
~/Documents/secrets/url_shortening_service/ on the control node.
Generate tokens and peppers with:
python -c "import secrets; print(secrets.token_urlsafe(32))"
Never reuse DJANGO_SECRET_KEY as an API token. Never put the token in the
short URL, logs, or git.
Deploy notes
Two NPM hosts, same container WEB_PORT (default 8005 prod / 8015 beta):
SHORT_DOMAIN(aiml.pw/cidinn.li) —/landing +GET /[a-z0-9]{4,8}. Drop/api/,/admin/,/debug/.- API hostname — proxy
/api/only. Drop/admin/. Add that Host toDJANGO_ALLOWED_HOSTSandSHORT_API_HOSTS.