homeio
PublikFork dari doctor-io/homeio
architecture.md
Homeio is a Next.js 16 App Router application hosted by a custom Node HTTP server. The same process serves pages, API routes, SSE streams, and the terminal WebSocket upgrade path. Server-only domain logic lives under lib/server/; client feature slices live under modules/; shared API contracts and query keys live under lib/shared/.
flowchart LR
Browser["Browser: React desktop shell"] -->|"HTTP fetch"| Next["Next.js App Router routes"]
Browser -->|"EventSource SSE"| SSE["SSE route handlers"]
Browser -->|"WebSocket /api/terminal"| WS["Terminal WebSocket server"]
Next --> Services["lib/server/modules/* services"]
SSE --> Services
WS --> PTY["@lydell/node-pty"]
Services --> PG["PostgreSQL via Drizzle"]
Services --> Docker["Docker daemon via socket/CLI"]
Services --> FS["FILES_ROOT / DATA filesystem"]
Services --> UploadSidecar["Go upload-server"]
UploadSidecar -->|"Unix socket or TCP"| FS
Services --> HelperClient["network helper client"]
HelperClient -->|"Unix socket JSON protocol"| DbusHelper["services/dbus-helper/*.mjs"]
DbusHelper -->|"D-Bus"| NetworkManager["NetworkManager / udisks2"]
Process Model
| Process | Entry point | Role | Communication |
|---|---|---|---|
| Main app dev process | server.ts via npm run dev | Next.js app, API routes, SSE, terminal WebSocket | HTTP, SSE, WebSocket, Docker socket/CLI, PostgreSQL |
| Main app production process | dist-server/server.js via npm run start | Compiled custom server | Same as dev |
| Docker database | postgres:16-alpine in docker-compose.yml | PostgreSQL persistence | TCP through DATABASE_URL |
| D-Bus helper sidecar | services/dbus-helper/index.mjs | NetworkManager/udisks2 bridge | Unix socket at DBUS_HELPER_SOCKET_PATH, then D-Bus |
| Upload sidecar | services/upload-server/main.go | Bare-metal large upload writer | Unix socket at UPLOAD_SERVER_ADDR or TCP address |
| Docker daemon | host daemon | Compose app lifecycle, stats, logs | /var/run/docker.sock and Docker CLI |
server.ts wraps next.getRequestHandler() with compression, disables compression for stream paths, and attaches the terminal WebSocket server by importing lib/server/modules/terminal/websocket-server.ts.
Startup Sequence
server.tscomputes host/port fromHOMEIO_HTTP_HOST,HOMEIO_HTTP_PORT, orPORT.next({ dev, hostname, port, turbopack: false })is created.await app.prepare()initializes Next.js.ensureFilesRootDirectories()is imported fromlib/server/modules/files/path-resolver.tsand run asynchronously.createServer()creates the raw HTTP server.initializeWebSocketServer(server)fromlib/server/modules/terminal/websocket-server.tsattaches the/api/terminalupgrade handler.server.listen(port, hostname)starts serving requests.SIGINTandSIGTERMcallcloseServerGracefully()fromlib/server/http/graceful-shutdown.ts.
Request Lifecycles
Standard HTTP API
sequenceDiagram
participant B as Browser
participant S as server.ts
participant R as app/api route.ts
participant M as lib/server module
participant D as PostgreSQL/Docker/FS
B->>S: HTTP request
S->>R: Next.js handler
R->>R: createRequestId()
R->>M: service call
M->>D: query or system call
D-->>M: result
M-->>R: typed value
R-->>B: NextResponse.json({ data }) or { error }
Most routes return { data: ... }, but current code is not fully uniform. Examples: app/api/v1/files/route.ts returns { data, meta }; app/api/v1/scheduled-tasks/route.ts returns { tasks }; auth routes use auth-specific shapes. New routes should prefer { data: ... } and typed contracts in lib/shared/contracts/.
File Uploads
app/api/v1/files/upload/route.ts authenticates with requireApiSession(), stream-parses multipart bodies with busboy, and pipes file streams into lib/server/modules/files/service.ts without buffering full file contents in memory. The client sends path and includeHidden fields before file parts; upload progress and cancellation are handled by the XHR helper in modules/files/hooks/files-api.ts.
Bare-metal installs also build services/upload-server/main.go into ${INSTALL_DIR}/bin/upload-server. The systemd unit listens on UPLOAD_SERVER_ADDR=/run/home-server/upload.sock, validates the same homeio_session HMAC cookie format with AUTH_SESSION_SECRET, applies its own FILES_ROOT path jail, writes files directly to disk, and exposes /health. Install/update scripts write the unit and report service status.
SSE
SSE routes use ReadableStream, toSseChunk() from lib/server/realtime/sse.ts, serverEnv.SSE_HEARTBEAT_MS, and request.signal cleanup. Confirmed event names include:
| Route | Events |
|---|---|
app/api/v1/system/stream/route.ts | metrics.updated, heartbeat |
app/api/v1/docker/stats/stream/route.ts | stats.updated, heartbeat |
app/api/v1/apps/[appId]/logs/stream/route.ts | log.line, log.end, log.error, heartbeat |
app/api/v1/store/operations/[operationId]/stream/route.ts | operation.step, operation.completed, operation.failed, heartbeat |
app/api/v1/network/events/stream/route.ts | network event type from NetworkEvent, heartbeat |
app/api/v1/notifications/stream/route.ts | notification event type, heartbeat |
app/api/v1/files/usb/stream/route.ts | USB event type, heartbeat |
WebSocket Terminal
lib/server/modules/terminal/websocket-server.ts handles upgrade for /api/terminal. When serverEnv.TERMINAL_WS_REQUIRE_AUTH is true, it authenticates the homeio_session cookie via authenticateSession(). It starts a host PTY with bash or powershell.exe, supports Docker attach with docker exec -it, and enforces TERMINAL_MAX_SESSIONS_PER_USER, TERMINAL_IDLE_TIMEOUT_MS, and TERMINAL_MAX_SESSION_MS.
App Install Operation
sequenceDiagram
participant UI as App Store UI
participant API as store install route
participant Ops as lib/server/modules/apps/operations.ts
participant DB as app_operations/app_stacks
participant Docker as Docker Compose
participant SSE as operation stream
participant N as notifications
UI->>API: POST /api/v1/store/apps/{appId}/install
API->>Ops: startStoreOperation({ action: "install" })
Ops->>DB: createStoreOperation()
Ops-->>API: operation id
API-->>UI: { data: operation }
UI->>SSE: GET /api/v1/store/operations/{operationId}/stream
Ops->>Ops: queueMicrotask(execute)
Ops->>DB: patch step/progress
Ops-->>SSE: operation.step
Ops->>Docker: materialize compose, pull, up
Docker-->>Ops: status/logs
Ops->>DB: mark success or error
Ops-->>SSE: operation.completed or operation.failed
Ops->>N: createNotification()
Operations run in-process via queueMicrotask() in lib/server/modules/apps/operations.ts. There is no external worker. The module keeps an in-memory activeOperationsByApp guard, rejects concurrent operations for the same app with 409 operation_conflict, and rejects global concurrency above serverEnv.STORE_MAX_CONCURRENT_OPERATIONS with 429 operation_limit_reached.
Boundaries
lib/server/**is server-only. Files there must import"server-only"first.lib/shared/**is safe for client and server. API contracts andqueryKeysbelong here.lib/ui/**,lib/desktop/**,lib/client/**, andmodules/**are client-facing unless a file is explicitly server-only.@/*maps to the repo root. It is convenient but dangerous:@/lib/server/...can be imported from client files unless the localserver-onlypackage catches it.
Build Pipeline
| File | Purpose |
|---|---|
tsconfig.json | Next.js TypeScript config, strict mode, noEmit: true, @/* alias |
tsconfig.server.json | Compiles server.ts, lib/server/**/*.ts, and lib/shared/**/*.ts to CommonJS in dist-server/ |
next.config.mjs | Next build options; NEXT_OUTPUT=standalone enables standalone output for Docker |
package.json | npm run build runs next build then npm run build:server |
dist-server/ is the compiled custom Node server used by npm run start and the Docker runner. tsc-alias rewrites @/* aliases after the server build.
next.config.mjs sets both experimental.serverActions.bodySizeLimit and experimental.proxyClientMaxBodySize to 10gb. The proxy setting is required for large POST /api/v1/files/upload route-handler requests; Server Actions use a separate limit. The route itself now streams multipart data, but upstream proxies can still reject requests before Homeio receives them.
Deployment Topology
- Docker Compose:
docker-compose.ymlrunshomeioplusdb; the app mounts/var/run/docker.sock,/DATA, and app stack storage. - Bare-metal:
scripts/install.shinstalls the app, Nginx, upload sidecar, and sidecar dependencies for Debian/Ubuntu/Raspberry Pi OS. Network, USB, and disk features depend on host D-Bus/storage tools. - Development:
npm run devstarts the custom server onlocalhost:3000unless overridden.
Current Architecture Risks
- No central
middleware.ts; API auth depends on each route calling the shared route helper. /api/v1/**routes userequireApiSession()except documented OAuth bootstrap/callback routes.lib/server/modules/docker/compose-runner.tsis large and manipulates YAML textually.lib/server/modules/apps/operations.tsruns long operations in-process with in-memory concurrency guards, not a durable external queue.- Production builds fail on TypeScript errors; do not reintroduce
typescript.ignoreBuildErrors.