637 lines
31 KiB
Markdown
Raw Normal View History

# DevPlace architecture atlas
Author: retoor <retoor@molodetz.nl>
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<br/>127.0.0.1:PORT to :80<br/>8 websocket upgrade locations"]
app["app container<br/>uvicorn devplacepy.main:app<br/>--workers 2 --backlog 8192"]
end
repo["repository root<br/>bind mount .:/app"]
static["devplacepy/static<br/>read only mount"]
data["DEVPLACE_DATA_DIR = data/<br/>23 registered paths"]
sqlite[("data/devplace.db<br/>SQLite, WAL, 256MB mmap")]
lock["data/locks/devplace-services.lock<br/>flock, elects one service owner"]
docker["host Docker daemon<br/>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<br/>creates all 23 DATA_PATHS"]
s2["init_lock<br/>exclusive flock on INIT_LOCK_FILE"]
s3["init_db<br/>idempotent columns and indexes"]
s4["ensure_certificates<br/>VAPID keys in data/keys"]
s5["service_manager.register x27"]
gate{"DEVPLACE_DISABLE_SERVICES set?"}
skip["services registered but never started<br/>test mode"]
bg["background.start<br/>per worker asyncio queue"]
elect{"acquire_service_lock"}
owner["set_lock_owner True<br/>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<br/>minimum_size 512, level 5"]
m1["1 TunnelDispatchMiddleware<br/>host matches a workspace tunnel?"]
tun["routers/tunnel.py<br/>handle_http / handle_ws"]
m2["2 response_timing<br/>request.state.request_start, X-Response-Time"]
m3["3 visit_statistics"]
m4["4 track_presence<br/>throttled last_seen write"]
m5["5 maintenance_middleware<br/>503 unless static, avatar, auth, admin"]
m6["6 rate_limit_middleware<br/>mutating methods only, per IP bucket"]
m7["7 add_security_headers"]
m8["8 await_pending_corrections<br/>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<br/>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<br/>Pydantic form model via Annotated Form"]
guard["guard: get_current_user<br/>require_user / require_admin"]
handler["router handler"]
helpers["database/ batch helpers<br/>get_users_by_uids, build_pagination"]
ctx["context dict"]
neg{"wants_json(request)"}
json["model.model_validate<br/>JSONResponse"]
html["templates.TemplateResponse<br/>extends base.html"]
action["services/devii/actions catalog<br/>Action, requires_auth mirrors guard"]
docs["docs_api endpoint()<br/>params and sample_response"]
seo["seo.py base_seo_context<br/>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<br/>64 hex chars"}
xapi{"X-API-KEY header"}
bearer{"Authorization: Bearer"}
basic{"Authorization: Basic"}
triple["try in order:<br/>users.api_key,<br/>devrant token 40 hex,<br/>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<br/>supervise, shutdown_all"]
settings[("site_settings<br/>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<br/>access_tokens, devrant_tokens<br/>user_relations, follows<br/>notification_preferences, user_customizations<br/>push_registration, email_accounts<br/>telegram_links, telegram_pairings, telegram_outbox"]
end
subgraph g2["Content and engagement - 16"]
t2["posts, comments, gists<br/>projects, project_files, project_forks<br/>attachments, news, news_images, news_sync<br/>polls, poll_options, poll_votes<br/>votes, reactions, bookmarks"]
end
subgraph g3["Reputation - 5"]
t3["awards, badges, award_usage<br/>user_activity, user_activity_seen"]
end
subgraph g4["Code Farm - 9"]
t4["game_farms, game_plots, game_quests<br/>game_cosmetics, game_market_ticks<br/>game_steals, game_treasury<br/>game_eras, game_era_results"]
end
subgraph g5["Quizzes - 5"]
t5["quizzes, quiz_questions, quiz_options<br/>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<br/>devii_tasks, devii_task_runs<br/>devii_lessons, devii_virtual_tools<br/>devii_behavior, devii_usage_ledger"]
end
subgraph g8["AI gateway - 6"]
t8["gateway_providers, gateway_models<br/>gateway_usage_ledger<br/>gateway_quota_rules, gateway_quota_resets<br/>gateway_concurrency_samples"]
end
subgraph g9["Jobs, tools and usage - 20"]
t9["jobs, backups, backup_schedules<br/>deepsearch_sessions, deepsearch_messages, deepsearch_url_cache<br/>isslop_analyses, isslop_reports, isslop_events<br/>isslop_dom_results, isslop_file_results, isslop_image_results<br/>seo_metadata, seo_usage<br/>issue_tickets, issue_comment_authors, issue_usage<br/>correction_usage, modifier_usage, news_usage"]
end
subgraph g10["Containers - 9"]
t10["instances, instance_events, instance_metrics<br/>tunnels, workspace_flags, workspace_quota_rules<br/>builds, dockerfiles, dockerfile_versions"]
end
subgraph g11["Platform state - 7"]
t11["site_settings, cache_state, service_state<br/>audit_log, audit_log_links<br/>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<br/>default repo/data"]
blobs["uploads, attachments, project_files<br/>sharded xx/yy on the uuid7 random tail"]
jobsdir["zips, zip_staging, fork_staging<br/>backups, backup_staging"]
reports["seo_reports, planning_reports<br/>dbapi, deepsearch, deepsearch_chroma"]
isslop["isslop, isslop_workspaces<br/>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<br/>/openai/v1/*"]
subgraph consumers["Internal consumers via INTERNAL_GATEWAY_URL"]
c1["services/devii"]
c2["services/correction<br/>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<br/>routing.py, quota.py, usage.py<br/>reliability.py, vision.py"]
ledger[("gateway_usage_ledger<br/>gateway_quota_rules")]
stealth["stealth_async_client<br/>curl_cffi Chrome 146 fingerprint"]
up["upstream provider"]
subgraph devii["Devii assistant"]
d1["registry.py CATALOG<br/>318 tools"]
d2["role gating<br/>guest 108, member 246<br/>admin 312, primary 318"]
d3["45 tools behind CONFIRM_REQUIRED"]
d4["session, tasks, lessons<br/>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<br/>instances, schedules, workspace"]
api["services/containers/api.py"]
store[("instances, instance_events<br/>instance_metrics, tunnels<br/>workspace_flags, workspace_quota_rules")]
svc["ContainerService<br/>reconcile desired vs docker ps"]
wsvc["WorkspaceService<br/>provision, certs, quota, tunnels"]
backend["backend/docker_cli.py<br/>backend/fake.py for tests"]
image["single shared image ppy:latest"]
inst["running instance<br/>rootless workflow, aptroot"]
wsdir["data/container_workspaces/{uid}"]
proxy["/p/{slug}<br/>routers/proxy.py, relays headers verbatim"]
tunnel["host based tunnel<br/>TunnelDispatchMiddleware to routers/tunnel.py"]
code["workspace editor over websocket<br/>/projects/{slug}/containers/instances/{uid}/code"]
exec["terminal over websocket<br/>.../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<br/>track limit 500, online limit 30"]
r4["PubSubService"]
end
hubs["messaging/hub.py, pubsub/hub.py<br/>devii/hub.py"]
tick[("ws_tickets<br/>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<br/>extra_head for CSS, extra_js for JS"]
partials["shared partials<br/>_avatar_link, _user_link, _comment_section<br/>_post_card, _pagination, _sidebar_search"]
appjs["static/js/Application.js<br/>instantiated once as app"]
subgraph modules["131 ES6 modules"]
u1["utilities<br/>Http, Poller, JobPoller<br/>OptimisticAction, FloatingWindow, ScrollMemory"]
u2["33 custom elements<br/>components/ with dp- prefix"]
u3["feature modules<br/>chat/, devii/, autoload/"]
end
css["49 stylesheets<br/>variables.css tokens, per page files"]
vers["static_url in Jinja, assetUrl in JS<br/>/static/v{boot ts}/, immutable for a year"]
cust["per user CSS and JS injection<br/>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<br/>mirrors source module path<br/>local_db or no fixture"]
api["tests/api - 221 modules<br/>mirrors URL path<br/>app_server, seeded_db"]
e2e["tests/e2e - 131 modules<br/>mirrors URL path<br/>page, alice, bob"]
srv["uvicorn subprocess :10501<br/>temp SQLite, own DATA_DIR<br/>DEVPLACE_DISABLE_SERVICES=1"]
pw["Playwright Chromium<br/>session scoped context"]
ci["Gitea Actions on push and PR to master<br/>full suite under coverage"]
prod["promotion to production<br/>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.