# DevPlace architecture atlas Author: retoor Every diagram below was derived from the source tree, the live SQLite schema and the imported FastAPI app object, not from the documentation. Where the repository documentation and the running code disagree, the disagreement is recorded in the last section. | Measure | Value | |---|---| | python files | 644 | | routers | 38 | | http paths | 348 | | websockets | 12 | | services | 27 | | db tables | 102 | | agent tools | 318 | | test functions | 3073 | ## 01 Deployment topology Source: `docker-compose.yml / Dockerfile / nginx/nginx.conf.template` Two containers on one bridge network, on a single host. nginx is the only published listener and binds to loopback only. The app container bind-mounts the repository root, so the production process reads the same SQLite file and the same `data/` tree as `make dev`. ```mermaid %%{init:{"theme":"base","themeVariables":{"background":"#F4F8F7","primaryColor":"#E4EEED","primaryTextColor":"#0F1A1B","primaryBorderColor":"#0A6D6A","secondaryColor":"#EDE6D9","tertiaryColor":"#F4F8F7","lineColor":"#6B7C7C","textColor":"#0F1A1B","fontFamily":"ui-monospace,SFMono-Regular,Menlo,monospace","fontSize":"13px","clusterBkg":"#FAFCFC","clusterBorder":"#C6D4D3"}}}%% flowchart TB client["Browser / API client / Telegram / devRant client"] subgraph host["Single host"] subgraph appnet["docker network: appnet"] nginx["nginx container
127.0.0.1:PORT to :80
8 websocket upgrade locations"] app["app container
uvicorn devplacepy.main:app
--workers 2 --backlog 8192"] end repo["repository root
bind mount .:/app"] static["devplacepy/static
read only mount"] data["DEVPLACE_DATA_DIR = data/
23 registered paths"] sqlite[("data/devplace.db
SQLite, WAL, 256MB mmap")] lock["data/locks/devplace-services.lock
flock, elects one service owner"] docker["host Docker daemon
ppy:latest instances"] end client --> nginx nginx -->|"proxy_pass, X-Real-IP"| app nginx -->|"/static/, /static/uploads/"| static app --> repo app --> data data --> sqlite app --> lock app -->|"docker CLI backend"| docker ``` nginx depends_on app with condition service_healthy. App healthcheck: interval 30s, timeout 10s, retries 3, start_period 120s, start_interval 2s. ## 02 Worker boot and service election Source: `devplacepy/main.py lifespan` Every uvicorn worker runs the full lifespan. Schema work is serialized behind an exclusive lock so workers pay it one after another; background services are elected, so exactly one worker in the host owns them. ```mermaid %%{init:{"theme":"base","themeVariables":{"background":"#F4F8F7","primaryColor":"#E4EEED","primaryTextColor":"#0F1A1B","primaryBorderColor":"#0A6D6A","secondaryColor":"#EDE6D9","tertiaryColor":"#F4F8F7","lineColor":"#6B7C7C","textColor":"#0F1A1B","fontFamily":"ui-monospace,SFMono-Regular,Menlo,monospace","fontSize":"13px","clusterBkg":"#FAFCFC","clusterBorder":"#C6D4D3"}}}%% flowchart TB s1["ensure_data_dirs
creates all 23 DATA_PATHS"] s2["init_lock
exclusive flock on INIT_LOCK_FILE"] s3["init_db
idempotent columns and indexes"] s4["ensure_certificates
VAPID keys in data/keys"] s5["service_manager.register x27"] gate{"DEVPLACE_DISABLE_SERVICES set?"} skip["services registered but never started
test mode"] bg["background.start
per worker asyncio queue"] elect{"acquire_service_lock"} owner["set_lock_owner True
supervise all 27 services"] decline["declined, another worker owns them"] vis["start_visit_flusher"] serve(["serving on port 10500"]) s1 --> s2 --> s3 --> s4 --> s5 --> gate gate -->|yes| skip --> vis gate -->|no| bg --> elect elect -->|"lock acquired"| owner --> vis elect -->|"lock held elsewhere"| decline --> vis vis --> serve ``` Shutdown reverses it: flush_visits, service_manager.shutdown_all, background.stop. ## 03 Request pipeline Source: `devplacepy/main.py, verified against app.user_middleware` Ten middlewares wrap every request. The order below is the real execution order read off the built middleware stack, outermost first. Two of them are plain ASGI classes; the other eight are `BaseHTTPMiddleware` dispatch functions. ```mermaid %%{init:{"theme":"base","themeVariables":{"background":"#F4F8F7","primaryColor":"#E4EEED","primaryTextColor":"#0F1A1B","primaryBorderColor":"#0A6D6A","secondaryColor":"#EDE6D9","tertiaryColor":"#F4F8F7","lineColor":"#6B7C7C","textColor":"#0F1A1B","fontFamily":"ui-monospace,SFMono-Regular,Menlo,monospace","fontSize":"13px","clusterBkg":"#FAFCFC","clusterBorder":"#C6D4D3"}}}%% flowchart TB req(["incoming ASGI scope"]) m0["0 GZipMiddleware
minimum_size 512, level 5"] m1["1 TunnelDispatchMiddleware
host matches a workspace tunnel?"] tun["routers/tunnel.py
handle_http / handle_ws"] m2["2 response_timing
request.state.request_start, X-Response-Time"] m3["3 visit_statistics"] m4["4 track_presence
throttled last_seen write"] m5["5 maintenance_middleware
503 unless static, avatar, auth, admin"] m6["6 rate_limit_middleware
mutating methods only, per IP bucket"] m7["7 add_security_headers"] m8["8 await_pending_corrections
drains sync AI correction futures"] m9["9 refresh_db_snapshot"] mounts["mounts: /static/uploads, /static/v{ts}, /static"] routes["38 included routers"] handlers["exception handlers
404, 500, RequestValidationError"] req --> m0 --> m1 m1 -->|"tunnel host"| tun m1 -->|"normal host"| m2 --> m3 --> m4 --> m5 --> m6 --> m7 --> m8 --> m9 m9 --> mounts m9 --> routes routes --> handlers ``` Rate limiting reads rate_limit_per_minute and rate_limit_window_seconds from site_settings and exempts reads and the /openai gateway. Bucket key is X-Real-IP falling back to request.client.host. ## 04 URL surface Source: `devplacepy/main.py include_router calls` 38 routers, each mounted under one prefix, grouped here by the concern they serve. Directory shape mirrors the URL: a domain with one resource is a flat module, a domain with several sub-resources is a package that aggregates its leaves in `__init__.py`. ```mermaid %%{init:{"theme":"base","themeVariables":{"background":"#F4F8F7","primaryColor":"#E4EEED","primaryTextColor":"#0F1A1B","primaryBorderColor":"#0A6D6A","secondaryColor":"#EDE6D9","tertiaryColor":"#F4F8F7","lineColor":"#6B7C7C","textColor":"#0F1A1B","fontFamily":"ui-monospace,SFMono-Regular,Menlo,monospace","fontSize":"12px","clusterBkg":"#FAFCFC","clusterBorder":"#C6D4D3"}}}%% flowchart LR app(["FastAPI app"]) subgraph identity["Identity and profile"] r1["/auth - auth/"] r2["/profile - profile/"] r3["/avatar - avatar.py"] r4["/follow - follow.py"] r5["(none) - relations.py block, mute"] end subgraph social["Content and engagement"] r6["/feed - feed.py"] r7["/posts - posts.py"] r8["/comments - comments.py"] r9["/gists - gists.py"] r10["/news - news.py"] r11["/votes - votes.py"] r12["/reactions - reactions.py"] r13["/bookmarks - bookmarks.py"] r14["/polls - polls.py"] r15["/awards - awards.py"] r16["/leaderboard - leaderboard.py"] r17["/notifications - notifications.py"] r18["/messages - messages.py"] r19["/uploads - uploads.py"] r20["/media - media.py"] end subgraph work["Projects and compute"] r21["/projects - projects/ incl files, containers"] r22["/p - proxy.py container ingress"] r23["/zips - zips.py"] r24["/forks - forks.py"] r25["/issues - issues/"] end subgraph aiml["AI and tools"] r26["/openai - openai_gateway.py"] r27["/devii - devii.py"] r28["/tools - tools/ seo, deepsearch, isslop"] end subgraph play["Play"] r29["/game - game/"] r30["/quizzes - quizzes/"] end subgraph platform["Platform"] r31["/admin - admin/ 23 modules"] r32["/docs - docs/"] r33["(none) - push.py, seo.py"] end subgraph machine["Machine interfaces"] r34["/api - devrant/"] r35["/dbapi - dbapi/ read only"] r36["/xmlrpc - xmlrpc.py"] r37["/pubsub - pubsub.py"] end app --> identity app --> social app --> work app --> aiml app --> play app --> platform app --> machine ``` | Prefix | Paths | Prefix | Paths | Prefix | Paths | |---|---|---|---|---|---| | /admin | 92 | /profile | 17 | /gists | 5 | | /projects | 46 | /issues | 13 | /messages | 5 | | /tools | 27 | /dbapi | 9 | /notifications | 5 | | /game | 23 | /auth | 6 | /uploads | 5 | | /quizzes | 21 | /devii | 5 | /docs | 4 | | /api | 20 | /posts | 4 | everything else | 36 | ## 05 Route fan-out Source: `devplacepy/responses.py, schemas/, docs_api/, services/devii/actions/` One handler serves four consumers. `respond()` feeds the same context dict to a Pydantic model and to a Jinja template, so a key missing from the `*Out` schema is silently absent from JSON while still rendering in HTML. The agent catalog and the docs registry are separate declarations that must be kept in step by hand. ```mermaid %%{init:{"theme":"base","themeVariables":{"background":"#F4F8F7","primaryColor":"#E4EEED","primaryTextColor":"#0F1A1B","primaryBorderColor":"#0A6D6A","secondaryColor":"#EDE6D9","tertiaryColor":"#F4F8F7","lineColor":"#6B7C7C","textColor":"#0F1A1B","fontFamily":"ui-monospace,SFMono-Regular,Menlo,monospace","fontSize":"13px","clusterBkg":"#FAFCFC","clusterBorder":"#C6D4D3"}}}%% flowchart TB form["models.py
Pydantic form model via Annotated Form"] guard["guard: get_current_user
require_user / require_admin"] handler["router handler"] helpers["database/ batch helpers
get_users_by_uids, build_pagination"] ctx["context dict"] neg{"wants_json(request)"} json["model.model_validate
JSONResponse"] html["templates.TemplateResponse
extends base.html"] action["services/devii/actions catalog
Action, requires_auth mirrors guard"] docs["docs_api endpoint()
params and sample_response"] seo["seo.py base_seo_context
JSON-LD, sitemap entry"] form --> handler guard --> handler handler --> helpers --> ctx --> neg neg -->|"json accepted"| json neg -->|"otherwise"| html handler -.->|"same capability"| action handler -.->|"same contract"| docs html -.-> seo ``` wants_json is true when the request content-type starts with application/json, or Accept contains application/json and does not contain text/html. Registry sizes: 318 agent actions, 259 documented endpoint entries across 20 API groups, 122 prose docs pages. ## 06 Identity resolution Source: `devplacepy/utils/auth.py` A single resolver serves browsers, API clients, the devRant compatibility layer and issued access tokens. The result is memoised on `request.state` for the request and in a process TTL cache keyed by credential. ```mermaid %%{init:{"theme":"base","themeVariables":{"background":"#F4F8F7","primaryColor":"#E4EEED","primaryTextColor":"#0F1A1B","primaryBorderColor":"#0A6D6A","secondaryColor":"#EDE6D9","tertiaryColor":"#F4F8F7","lineColor":"#6B7C7C","textColor":"#0F1A1B","fontFamily":"ui-monospace,SFMono-Regular,Menlo,monospace","fontSize":"13px","clusterBkg":"#FAFCFC","clusterBorder":"#C6D4D3"}}}%% flowchart TB start(["get_current_user"]) cached{"request.state._auth_user set?"} sess{"session cookie
64 hex chars"} xapi{"X-API-KEY header"} bearer{"Authorization: Bearer"} basic{"Authorization: Basic"} triple["try in order:
users.api_key,
devrant token 40 hex,
access token 64 chars"] user(["user dict"]) guest(["None, guest"]) gu["require_user: 303 to /"] ga["require_admin: redirect to /feed"] start --> cached cached -->|yes| user cached -->|no| sess sess -->|match| user sess -->|no| xapi xapi -->|present| triple triple -->|match| user triple -->|no match| bearer xapi -->|absent| bearer bearer -->|present| triple bearer -->|absent| basic basic -->|"username-or-email:password, pbkdf2_sha256"| user basic -->|no| guest guest --> gu user --> ga ``` Roles are stored capitalized as Admin or Member and tested through the is_admin global. Admin seniority is enforced per user mutation in routers/admin/users.py. ## 07 Background service fleet Source: `devplacepy/services/manager.py, base.py, jobs/base.py` 27 singletons registered at boot, supervised only by the worker that won the service lock. Sixteen are long-lived loops on `BaseService`; eleven are queue consumers on `JobService`, which adds retention, concurrency and timeout settings on top and drains rows from the shared `jobs` table. ```mermaid %%{init:{"theme":"base","themeVariables":{"background":"#F4F8F7","primaryColor":"#E4EEED","primaryTextColor":"#0F1A1B","primaryBorderColor":"#0A6D6A","secondaryColor":"#EDE6D9","tertiaryColor":"#F4F8F7","lineColor":"#6B7C7C","textColor":"#0F1A1B","fontFamily":"ui-monospace,SFMono-Regular,Menlo,monospace","fontSize":"12px","clusterBkg":"#FAFCFC","clusterBorder":"#C6D4D3"}}}%% flowchart LR mgr["ServiceManager
supervise, shutdown_all"] settings[("site_settings
enabled flag and interval per service")] jobs[("jobs table")] subgraph base["BaseService loops - 16"] b1["NewsService"] b2["BotsService"] b3["GatewayService"] b4["DeviiService"] b5["PubSubService"] b6["NotificationRelayService"] b7["LiveViewRelayService"] b8["PresenceRelayService"] b9["IssueTrackerService"] b10["ContainerService"] b11["WorkspaceService"] b12["XmlrpcService"] b13["AuditService"] b14["PushService"] b15["TelegramService"] b16["TelegramOutboxService"] end subgraph job["JobService consumers - 11"] j1["ZipService"] j2["ForkService"] j3["SeoService"] j4["SeoMetaService"] j5["AwardService"] j6["BackupService"] j7["DbApiJobService"] j8["DeepsearchService"] j9["IsslopService"] j10["IssueCreateService"] j11["PlanningReportService"] end mgr --> base mgr --> job settings --> mgr job --> jobs ``` Separate from the fleet, services/background.py offers a fire and forget queue per worker for audit writes, XP awards and notifications; when no consumer runs, as in tests, the callable executes inline so ordering stays deterministic. ## 08 Data layer Source: `devplacepy/database/, devplacepy/config.py` One SQLite file reached through `dataset`, called synchronously from async handlers by design. 102 tables exist in the live schema; 52 of them carry the soft delete pair and are restorable as one event from the admin trash. ```mermaid %%{init:{"theme":"base","themeVariables":{"background":"#F4F8F7","primaryColor":"#E4EEED","primaryTextColor":"#0F1A1B","primaryBorderColor":"#0A6D6A","secondaryColor":"#EDE6D9","tertiaryColor":"#F4F8F7","lineColor":"#6B7C7C","textColor":"#0F1A1B","fontFamily":"ui-monospace,SFMono-Regular,Menlo,monospace","fontSize":"12px","clusterBkg":"#FAFCFC","clusterBorder":"#C6D4D3"}}}%% flowchart TB db[("data/devplace.db")] subgraph g1["Identity and access - 14"] t1["users, sessions, password_resets
access_tokens, devrant_tokens
user_relations, follows
notification_preferences, user_customizations
push_registration, email_accounts
telegram_links, telegram_pairings, telegram_outbox"] end subgraph g2["Content and engagement - 16"] t2["posts, comments, gists
projects, project_files, project_forks
attachments, news, news_images, news_sync
polls, poll_options, poll_votes
votes, reactions, bookmarks"] end subgraph g3["Reputation - 5"] t3["awards, badges, award_usage
user_activity, user_activity_seen"] end subgraph g4["Code Farm - 9"] t4["game_farms, game_plots, game_quests
game_cosmetics, game_market_ticks
game_steals, game_treasury
game_eras, game_era_results"] end subgraph g5["Quizzes - 5"] t5["quizzes, quiz_questions, quiz_options
quiz_attempts, quiz_answers"] end subgraph g6["Live delivery - 3"] t6["messages, notifications, ws_tickets"] end subgraph g7["Devii - 8"] t7["devii_conversations, devii_turns
devii_tasks, devii_task_runs
devii_lessons, devii_virtual_tools
devii_behavior, devii_usage_ledger"] end subgraph g8["AI gateway - 6"] t8["gateway_providers, gateway_models
gateway_usage_ledger
gateway_quota_rules, gateway_quota_resets
gateway_concurrency_samples"] end subgraph g9["Jobs, tools and usage - 20"] t9["jobs, backups, backup_schedules
deepsearch_sessions, deepsearch_messages, deepsearch_url_cache
isslop_analyses, isslop_reports, isslop_events
isslop_dom_results, isslop_file_results, isslop_image_results
seo_metadata, seo_usage
issue_tickets, issue_comment_authors, issue_usage
correction_usage, modifier_usage, news_usage"] end subgraph g10["Containers - 9"] t10["instances, instance_events, instance_metrics
tunnels, workspace_flags, workspace_quota_rules
builds, dockerfiles, dockerfile_versions"] end subgraph g11["Platform state - 7"] t11["site_settings, cache_state, service_state
audit_log, audit_log_links
visit_stats_hourly, visit_unique_slots"] end db --> g1 db --> g2 db --> g3 db --> g4 db --> g5 db --> g6 db --> g7 db --> g8 db --> g9 db --> g10 db --> g11 ``` ```mermaid %%{init:{"theme":"base","themeVariables":{"background":"#F4F8F7","primaryColor":"#E4EEED","primaryTextColor":"#0F1A1B","primaryBorderColor":"#0A6D6A","secondaryColor":"#EDE6D9","tertiaryColor":"#F4F8F7","lineColor":"#6B7C7C","textColor":"#0F1A1B","fontFamily":"ui-monospace,SFMono-Regular,Menlo,monospace","fontSize":"12px","clusterBkg":"#FAFCFC","clusterBorder":"#C6D4D3"}}}%% flowchart LR root["DEVPLACE_DATA_DIR
default repo/data"] blobs["uploads, attachments, project_files
sharded xx/yy on the uuid7 random tail"] jobsdir["zips, zip_staging, fork_staging
backups, backup_staging"] reports["seo_reports, planning_reports
dbapi, deepsearch, deepsearch_chroma"] isslop["isslop, isslop_workspaces
isslop_runs, isslop_media"] ws["container_workspaces, workspace_state"] keys["keys VAPID, bot, locks"] dbs["devplace.db, devii_tasks.db, devii_lessons.db"] root --> blobs root --> jobsdir root --> reports root --> isslop root --> ws root --> keys root --> dbs ``` config.DATA_PATHS registers 23 directories and ensure_data_dirs creates the whole tree before any write. Uploads live under data/uploads but are served at the unchanged /static/uploads URL. ## 09 Content rendering Source: `devplacepy/rendering.py, static/js/ContentRenderer.js` Two pipelines with a deliberate split: anything that exists at request time is rendered on the server for SEO, and the client pipeline is reserved for content that does not exist yet. Each has its own XSS control at a different point. ```mermaid %%{init:{"theme":"base","themeVariables":{"background":"#F4F8F7","primaryColor":"#E4EEED","primaryTextColor":"#0F1A1B","primaryBorderColor":"#0A6D6A","secondaryColor":"#EDE6D9","tertiaryColor":"#F4F8F7","lineColor":"#6B7C7C","textColor":"#0F1A1B","fontFamily":"ui-monospace,SFMono-Regular,Menlo,monospace","fontSize":"13px","clusterBkg":"#FAFCFC","clusterBorder":"#C6D4D3"}}}%% flowchart TB subgraph server["Server, rendering.py, lru_cache"] a1["raw text"] a2["normalize dashes to hyphen"] a3["emoji shortcodes, 4869 names"] a4["mistune GFM, escape=True"] a5["media pass: bare URLs to embeds, mentions to links"] a6["mask emails on rendered text nodes"] a7["render_content / render_title in template"] a8["ContentEnhancer adds highlighting and copy buttons"] a1 --> a2 --> a3 --> a4 --> a5 --> a6 --> a7 --> a8 end subgraph client["Client, ContentRenderer.js"] b1["live text: comments, DM bubbles, Devii, DeepSearch"] b2["marked"] b3["DOMPurify.sanitize, fail closed"] b4["highlight.js"] b5["media and autolink pass"] b1 --> b2 --> b3 --> b4 --> b5 end ``` The server escapes at the markdown step, the client sanitizes after parsing. Server rendered content must not carry data-render, or both pipelines run over it. ## 10 AI plane Source: `devplacepy/routers/openai_gateway.py, services/openai_gateway/, services/devii/` Every model call in the product, internal or external, leaves through one gateway, which is where routing, quota and cost attribution live. Internal consumers call it over loopback HTTP rather than importing a client, so their spend lands in the same ledger. ```mermaid %%{init:{"theme":"base","themeVariables":{"background":"#F4F8F7","primaryColor":"#E4EEED","primaryTextColor":"#0F1A1B","primaryBorderColor":"#0A6D6A","secondaryColor":"#EDE6D9","tertiaryColor":"#F4F8F7","lineColor":"#6B7C7C","textColor":"#0F1A1B","fontFamily":"ui-monospace,SFMono-Regular,Menlo,monospace","fontSize":"12px","clusterBkg":"#FAFCFC","clusterBorder":"#C6D4D3"}}}%% flowchart TB ext["external client
/openai/v1/*"] subgraph consumers["Internal consumers via INTERNAL_GATEWAY_URL"] c1["services/devii"] c2["services/correction
AI correction and modifier"] c3["services/news"] c4["services/bot"] c5["services/deepsearch + jobs/deepsearch"] c6["services/dbapi nl2sql"] c7["services/gitea enhance + planning"] c8["jobs/isslop"] end gw["gateway.py
routing.py, quota.py, usage.py
reliability.py, vision.py"] ledger[("gateway_usage_ledger
gateway_quota_rules")] stealth["stealth_async_client
curl_cffi Chrome 146 fingerprint"] up["upstream provider"] subgraph devii["Devii assistant"] d1["registry.py CATALOG
318 tools"] d2["role gating
guest 108, member 246
admin 312, primary 318"] d3["45 tools behind CONFIRM_REQUIRED"] d4["session, tasks, lessons
virtual tools, behavior"] d5["surfaces: /devii ws, Telegram, CLI"] end ext --> gw consumers --> gw gw --> ledger gw --> stealth --> up d1 --> d2 --> d3 devii --> c1 d4 --> d1 d5 --> d4 ``` Cleartext loopback calls are forced to HTTP/1.1 in curl_transport.http_version_for, because the Chrome impersonation profile would otherwise negotiate HTTP/2 against an HTTP/1.1 only uvicorn. ## 11 Containers, workspaces and ingress Source: `devplacepy/services/containers/, routers/projects/containers/, routers/proxy.py, routers/tunnel.py` One shared image serves every instance. Reconciliation is a loop that compares desired state in the database against what the Docker daemon actually reports, and there are two independent ways in from the outside: a path prefix and a hostname. ```mermaid %%{init:{"theme":"base","themeVariables":{"background":"#F4F8F7","primaryColor":"#E4EEED","primaryTextColor":"#0F1A1B","primaryBorderColor":"#0A6D6A","secondaryColor":"#EDE6D9","tertiaryColor":"#F4F8F7","lineColor":"#6B7C7C","textColor":"#0F1A1B","fontFamily":"ui-monospace,SFMono-Regular,Menlo,monospace","fontSize":"12px","clusterBkg":"#FAFCFC","clusterBorder":"#C6D4D3"}}}%% flowchart TB ui["/projects/{slug}/containers
instances, schedules, workspace"] api["services/containers/api.py"] store[("instances, instance_events
instance_metrics, tunnels
workspace_flags, workspace_quota_rules")] svc["ContainerService
reconcile desired vs docker ps"] wsvc["WorkspaceService
provision, certs, quota, tunnels"] backend["backend/docker_cli.py
backend/fake.py for tests"] image["single shared image ppy:latest"] inst["running instance
rootless workflow, aptroot"] wsdir["data/container_workspaces/{uid}"] proxy["/p/{slug}
routers/proxy.py, relays headers verbatim"] tunnel["host based tunnel
TunnelDispatchMiddleware to routers/tunnel.py"] code["workspace editor over websocket
/projects/{slug}/containers/instances/{uid}/code"] exec["terminal over websocket
.../exec/ws"] ui --> api --> store svc --> store wsvc --> store api --> backend svc --> backend wsvc --> wsdir backend --> image --> inst proxy --> inst tunnel --> inst code --> inst exec --> inst ``` Access uses stricter predicates than the rest of the product: owns_instance, can_view_project_containers, can_view_instance, can_manage_instance. The primary administrator sees and manages every container; any other admin can only view others on public projects and manage instances they own. ## 12 Real time plane Source: `routers/*.py websocket handlers, services relays, nginx.conf.template` Twelve WebSocket endpoints, each of which needs its own nginx upgrade location because the catch-all location strips upgrade headers. Fan-out to connected sockets goes through in-process hubs driven by relay services on the lock-owning worker. ```mermaid %%{init:{"theme":"base","themeVariables":{"background":"#F4F8F7","primaryColor":"#E4EEED","primaryTextColor":"#0F1A1B","primaryBorderColor":"#0A6D6A","secondaryColor":"#EDE6D9","tertiaryColor":"#F4F8F7","lineColor":"#6B7C7C","textColor":"#0F1A1B","fontFamily":"ui-monospace,SFMono-Regular,Menlo,monospace","fontSize":"12px","clusterBkg":"#FAFCFC","clusterBorder":"#C6D4D3"}}}%% flowchart LR subgraph sockets["WebSocket endpoints"] s1["/devii/ws"] s2["/messages/ws"] s3["/pubsub/ws"] s4["/tools/seo/{uid}/ws"] s5["/tools/deepsearch/{uid}/ws"] s6["/tools/deepsearch/{uid}/chat"] s7["/dbapi/query/{uid}/ws"] s8[".../containers/instances/{uid}/exec/ws"] s9[".../containers/instances/{uid}/code"] s10[".../code/{path}"] s11["/p/{slug}"] s12["/p/{slug}/{path}"] end subgraph relays["Relay services"] r1["NotificationRelayService"] r2["LiveViewRelayService"] r3["PresenceRelayService
track limit 500, online limit 30"] r4["PubSubService"] end hubs["messaging/hub.py, pubsub/hub.py
devii/hub.py"] tick[("ws_tickets
expiring auth tickets")] s2 --> hubs s3 --> hubs s1 --> hubs relays --> hubs tick --> s2 ``` Presence is authoritative from the relay: PRESENCE_TRACK_LIMIT sets the tracked online set, PRESENCE_ONLINE_LIMIT only caps how many the feed panel displays, and PRESENCE_ONLINE_MARGIN_SECONDS provides hysteresis at the boundary. ## 13 Frontend Source: `devplacepy/static/, devplacepy/templates/` No framework and no package manager. 131 ES6 modules, one class per file, hung off a single global `app`; 33 of them are custom elements. 217 Jinja templates, of which 107 are the docs site. ```mermaid %%{init:{"theme":"base","themeVariables":{"background":"#F4F8F7","primaryColor":"#E4EEED","primaryTextColor":"#0F1A1B","primaryBorderColor":"#0A6D6A","secondaryColor":"#EDE6D9","tertiaryColor":"#F4F8F7","lineColor":"#6B7C7C","textColor":"#0F1A1B","fontFamily":"ui-monospace,SFMono-Regular,Menlo,monospace","fontSize":"12px","clusterBkg":"#FAFCFC","clusterBorder":"#C6D4D3"}}}%% flowchart TB base["templates/base.html
extra_head for CSS, extra_js for JS"] partials["shared partials
_avatar_link, _user_link, _comment_section
_post_card, _pagination, _sidebar_search"] appjs["static/js/Application.js
instantiated once as app"] subgraph modules["131 ES6 modules"] u1["utilities
Http, Poller, JobPoller
OptimisticAction, FloatingWindow, ScrollMemory"] u2["33 custom elements
components/ with dp- prefix"] u3["feature modules
chat/, devii/, autoload/"] end css["49 stylesheets
variables.css tokens, per page files"] vers["static_url in Jinja, assetUrl in JS
/static/v{boot ts}/, immutable for a year"] cust["per user CSS and JS injection
custom_css_tag, custom_js_tag"] base --> partials base --> appjs --> modules base --> css base --> cust vers --> css vers --> modules ``` Per user customizations are configured only through Devii, stored in user_customizations, scoped globally or per matched route template, and run solely in that owner's own browser sessions. ## 14 Test and delivery topology Source: `tests/, pyproject.toml, .gitea/workflows/test.yaml` Three tiers separated by what they exercise, decided by fixtures, run serially in a single process against one uvicorn subprocess on port 10501 with a temporary database. ```mermaid %%{init:{"theme":"base","themeVariables":{"background":"#F4F8F7","primaryColor":"#E4EEED","primaryTextColor":"#0F1A1B","primaryBorderColor":"#0A6D6A","secondaryColor":"#EDE6D9","tertiaryColor":"#F4F8F7","lineColor":"#6B7C7C","textColor":"#0F1A1B","fontFamily":"ui-monospace,SFMono-Regular,Menlo,monospace","fontSize":"12px","clusterBkg":"#FAFCFC","clusterBorder":"#C6D4D3"}}}%% flowchart LR unit["tests/unit - 140 modules
mirrors source module path
local_db or no fixture"] api["tests/api - 221 modules
mirrors URL path
app_server, seeded_db"] e2e["tests/e2e - 131 modules
mirrors URL path
page, alice, bob"] srv["uvicorn subprocess :10501
temp SQLite, own DATA_DIR
DEVPLACE_DISABLE_SERVICES=1"] pw["Playwright Chromium
session scoped context"] ci["Gitea Actions on push and PR to master
full suite under coverage"] prod["promotion to production
docker compose"] unit --> ci api --> srv --> ci e2e --> srv e2e --> pw --> ci ci --> prod ``` pytest-xdist is not a dependency and -n is rejected centrally, so no tier can be parallelised by accident. ## 15 Findings Source: `measured against the repository documentation` The structure holds up: the router tree mirrors the URL tree, the test tree mirrors both, every runtime path is registered in one place, and every model call has a single exit. The items below are the places where the running code and the written record have drifted apart, or where a structure exists that nothing currently uses. None of them is a functional defect. ### Middleware order in the documentation is stale The root CLAUDE.md states that response_timing is the outermost middleware. Measured from app.user_middleware it is third, behind GZipMiddleware and TunnelDispatchMiddleware, both added after it. The consequence is minor but real: X-Response-Time excludes compression time and excludes tunnel-host dispatch entirely. ### Two catalogue counts have drifted CLAUDE.md cites around 2882 tests and an events.md catalogue of 288 keys. Measured now: 3073 functions named test_ across 492 modules, and 312 distinct dotted event keys in events.md. Both are undercounts in the docs, not missing implementation. ### instance_schedules is declared soft-deletable but has no table It appears in database.SOFT_DELETE_TABLES and in the routers under /projects/{slug}/containers/schedules, but no such table exists in the live schema. dataset creates it lazily on first insert, so this is correct only for as long as every read path tolerates the table being absent. ### Three legacy container tables still exist builds, dockerfiles and dockerfile_versions are present in the live database. The project replaced per-project images with the single shared ppy:latest image and ships devplace containers prune-builds as the one-time cleanup for exactly these rows. The tables are still carried. ### Five duplicate OpenAPI operation IDs Generating the schema warns on editor_proxy twice in routers/projects/containers/workspace.py, passthrough in routers/openai_gateway.py, and proxy_http twice in routers/proxy.py. These are catch-all routes registered for several methods, so a generated client would collide on those names. ### The one deliberate asymmetry is documented and intentional routers/__init__.py holds nothing but the attribution line; unlike every nested router package, the top level does not aggregate. main.py performs all 38 include_router calls directly, which keeps prefix ownership in a single readable block. --- Sources: devplacepy/main.py, the imported FastAPI app object, data/devplace.db sqlite_master, and the working tree at HEAD 192df12b with uncommitted local modifications present. Counts exclude __pycache__ and the virtual environment.