This file documents the devRant compatibility API mounted at /api. Claude Code auto-loads it whenever a file in routers/devrant/ is read or edited.
Routing overview
/api-devrant/package:devRant-compatible REST protocol (auth.py,rants.py,comments.py,notifs.py). Translates devRant requests onto DevPlace data: rants<->posts, comments/votes onto the native engagement layer, devRant integer ids onto each table's auto-incrementid. Token auth viadevrant_tokens.
devRant compatibility API (routers/devrant/, services/devrant/)
A second REST protocol mounted at /api that reproduces the public devRant API shape on DevPlace data, so legacy devRant clients (the ./devranta reference client is the spec; real captured responses are in ./devranta/api_test_results.json) run unchanged. It is a pure translation layer over the existing domain - it adds no new content model.
Layout. routers/devrant/ is the thin endpoint package: __init__.py builds the aggregate router with dependencies=[Depends(ensure_enabled)] (gated by the devrant_api_enabled setting, default on) and includes auth.py (login/register/profile/edit/avatar), rants.py (feed, CRUD, vote, favorite, comment, search), comments.py (read/edit/delete/vote), notifs.py (feed/clear); _shared.py holds the dr_ok/dr_error envelope helpers + the enable gate. services/devrant/ is the logic: params.merge_params (merges query string + body, accepting BOTH form-encoded and JSON since devRant clients use both, GET sends auth as query params), tokens (issue/validate/revoke the devrant_tokens triple), ids (as_int, to_unix, post_by_id/comment_by_id/user_by_id), serializers (serialize_rant/serialize_comment, encode_tags/decode_tags), feed (list_rants/search_rants/load_rant_detail, offset pagination via find(_limit, _offset)), profile (build_profile), notifications (build_notif_feed/clear_notifications), avatar (avatar_payload + render_png via cairosvg).
ID mapping (load-bearing). devRant integer ids ARE the auto-increment id PK every dataset table already has: rant_id=posts.id, comment_id=comments.id, user_id=users.id, token_id=devrant_tokens.id. No translation table exists - post_by_id is find_one(id=...). Serialization converts ISO created_at to unix via ids.to_unix.
Auth. POST /api/users/auth-token accepts username OR email, verifies with passlib, and inserts a devrant_tokens row (in SOFT_DELETE_TABLES; born-live; key=secrets.token_hex, expire_time from session_max_age_days). Every later call re-validates (token_id, token_key, user_id) through _shared.resolve_actor(request, params), which wraps tokens.resolve_user with utils.guards.refuse_suspended - because this path never touches require_user, a moderator's suspension would otherwise not bind here at all (the token resolver's is_account_active check covers a ban but not a time-boxed suspension). refuse_suspended gates mutating methods only, so read endpoints are unaffected. Read endpoints take an OPTIONAL viewer (it may return None); write endpoints return _shared.unauthorized() (401) when it does. DELETE /api/users/me deliberately calls the bare resolve_user - it is the account-deletion path and must stay reachable to a suspended user, matching the /profile/{username}/delete exemption on the web side.
Writes reuse the audited native cores - never duplicate. Implementing this drove four DRY extractions in content.py (apply_vote, create_comment_record, delete_comment_record, set_bookmark) and one in utils.py (register_account); the native routers/votes.py, routers/comments.py, and auth/signup.py were refactored onto the SAME functions. So a devRant rant/comment/vote awards XP, fires notifications, writes the audit row, and soft-deletes exactly like the UI path. Rant create calls content.create_content_item directly; rant delete calls content.delete_content_item (full cascade) and returns the devRant envelope.
Field mappings. Rant text = title\n\n content (inbound rants have no title, topic forced to "rant"). devRant tags round-trip verbatim through a new posts.tags JSON column (init_db ensures it; decode_tags falls back to [topic]). profile_skills is derived from bio (skills_from_bio, no native skills field). favorite/unfavorite map to bookmarks via set_bookmark. Avatars are real PNGs from the multiavatar engine at GET /api/avatars/u/{username}.png; user_avatar.i points at that path with a deterministic b background colour. The feed envelope includes the auxiliary devRant keys (settings, set, wrw, dpp, num_notifs, unread, news) with safe values so clients parse cleanly.
Envelope. dr_ok(**f) -> {success:true, **f}; dr_error(msg, status, **extra) -> {success:false, error:msg, ...}. Logical failures stay 200 except bad login -> 400 (matches the captured devRant behaviour). Reference client + captured responses live in ./devranta/. Host routing (legacy clients hard-code devrant.com) is an infra/DNS concern, out of app scope.
Cross-cutting note. AI content correction (devplacepy/services/correction.py) hooks the two devRant direct-edit paths alongside the native content/comment/messaging entrypoints, so it applies identically across the web UI, REST/JSON API, Devii, and devRant.