IKHSAN
ENID Baca dalam Bahasa Indonesia

homeio

Public

Forked from doctor-io/homeio

Branch
main
Latest commit
958d74616 October 2026

architecture.md

Open raw
Architecture

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

ProcessEntry pointRoleCommunication
Main app dev processserver.ts via npm run devNext.js app, API routes, SSE, terminal WebSocketHTTP, SSE, WebSocket, Docker socket/CLI, PostgreSQL
Main app production processdist-server/server.js via npm run startCompiled custom serverSame as dev
Docker databasepostgres:16-alpine in docker-compose.ymlPostgreSQL persistenceTCP through DATABASE_URL
D-Bus helper sidecarservices/dbus-helper/index.mjsNetworkManager/udisks2 bridgeUnix socket at DBUS_HELPER_SOCKET_PATH, then D-Bus
Upload sidecarservices/upload-server/main.goBare-metal large upload writerUnix socket at UPLOAD_SERVER_ADDR or TCP address
Docker daemonhost daemonCompose 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

  1. server.ts computes host/port from HOMEIO_HTTP_HOST, HOMEIO_HTTP_PORT, or PORT.
  2. next({ dev, hostname, port, turbopack: false }) is created.
  3. await app.prepare() initializes Next.js.
  4. ensureFilesRootDirectories() is imported from lib/server/modules/files/path-resolver.ts and run asynchronously.
  5. createServer() creates the raw HTTP server.
  6. initializeWebSocketServer(server) from lib/server/modules/terminal/websocket-server.ts attaches the /api/terminal upgrade handler.
  7. server.listen(port, hostname) starts serving requests.
  8. SIGINT and SIGTERM call closeServerGracefully() from lib/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:

RouteEvents
app/api/v1/system/stream/route.tsmetrics.updated, heartbeat
app/api/v1/docker/stats/stream/route.tsstats.updated, heartbeat
app/api/v1/apps/[appId]/logs/stream/route.tslog.line, log.end, log.error, heartbeat
app/api/v1/store/operations/[operationId]/stream/route.tsoperation.step, operation.completed, operation.failed, heartbeat
app/api/v1/network/events/stream/route.tsnetwork event type from NetworkEvent, heartbeat
app/api/v1/notifications/stream/route.tsnotification event type, heartbeat
app/api/v1/files/usb/stream/route.tsUSB 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 and queryKeys belong here.
  • lib/ui/**, lib/desktop/**, lib/client/**, and modules/** 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 local server-only package catches it.

Build Pipeline

FilePurpose
tsconfig.jsonNext.js TypeScript config, strict mode, noEmit: true, @/* alias
tsconfig.server.jsonCompiles server.ts, lib/server/**/*.ts, and lib/shared/**/*.ts to CommonJS in dist-server/
next.config.mjsNext build options; NEXT_OUTPUT=standalone enables standalone output for Docker
package.jsonnpm 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.yml runs homeio plus db; the app mounts /var/run/docker.sock, /DATA, and app stack storage.
  • Bare-metal: scripts/install.sh installs 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 dev starts the custom server on localhost:3000 unless overridden.

Current Architecture Risks

  • No central middleware.ts; API auth depends on each route calling the shared route helper.
  • /api/v1/** routes use requireApiSession() except documented OAuth bootstrap/callback routes.
  • lib/server/modules/docker/compose-runner.ts is large and manipulates YAML textually.
  • lib/server/modules/apps/operations.ts runs 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.
bytes
9,674
lines
157
Type
md
Latest commit
16 October 2026