From 4f174ec3a8d702d96b55b1235a38fab9bb8c9b75 Mon Sep 17 00:00:00 2001 From: Harivansh Rathi Date: Wed, 1 Apr 2026 03:11:43 +0000 Subject: [PATCH] skeleton schemas --- README.md | 23 ++- docs/architecture.md | 178 ++++++++++++++---- packages/contracts/README.md | 42 +++++ .../schemas/cloud-profile.schema.json | 30 +++ .../schemas/mount-profile.schema.json | 41 ++++ .../contracts/schemas/nas-node.schema.json | 52 +++++ .../schemas/storage-export.schema.json | 47 +++++ packages/contracts/src/foundation.ts | 97 ++++++++++ packages/contracts/src/index.ts | 2 +- 9 files changed, 470 insertions(+), 42 deletions(-) create mode 100644 packages/contracts/README.md create mode 100644 packages/contracts/schemas/cloud-profile.schema.json create mode 100644 packages/contracts/schemas/mount-profile.schema.json create mode 100644 packages/contracts/schemas/nas-node.schema.json create mode 100644 packages/contracts/schemas/storage-export.schema.json create mode 100644 packages/contracts/src/foundation.ts diff --git a/README.md b/README.md index 455b76a..e55291d 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,23 @@ image -## Architecture +## Start here -The intended boundary is documented in `docs/architecture.md`. The short version is: +The canonical repo contract lives in [docs/architecture.md](/home/rathi/Documents/GitHub/betterNAS/docs/architecture.md). -- Nextcloud remains an upstream storage and client-compatibility backend. -- The custom Nextcloud app is a shell and adapter layer. -- betterNAS business logic lives in the control-plane service. +Read these in order: + +1. [docs/architecture.md](/home/rathi/Documents/GitHub/betterNAS/docs/architecture.md) +2. [docs/01-nas-node.md](/home/rathi/Documents/GitHub/betterNAS/docs/01-nas-node.md) +3. [docs/02-control-plane.md](/home/rathi/Documents/GitHub/betterNAS/docs/02-control-plane.md) +4. [docs/03-local-device.md](/home/rathi/Documents/GitHub/betterNAS/docs/03-local-device.md) +5. [docs/04-cloud-web-layer.md](/home/rathi/Documents/GitHub/betterNAS/docs/04-cloud-web-layer.md) +6. [docs/05-build-plan.md](/home/rathi/Documents/GitHub/betterNAS/docs/05-build-plan.md) +7. [docs/references.md](/home/rathi/Documents/GitHub/betterNAS/docs/references.md) + +## Current direction + +- betterNAS is WebDAV-first for mount mode. +- the control plane is the system of record. +- the NAS node serves bytes directly whenever possible. +- Nextcloud is an optional cloud/web adapter, not the product center. diff --git a/docs/architecture.md b/docs/architecture.md index 5171619..da8d105 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,54 +1,160 @@ -# betterNAS Architecture Boundary +# betterNAS Architecture Contract -## Core Decision +This file is the canonical contract for the repository. -betterNAS treats Nextcloud as an upstream backend, not as the place where betterNAS product logic should accumulate. +If the planning docs, scaffold code, or future tasks disagree, this file and +[`packages/contracts`](/home/rathi/Documents/GitHub/betterNAS/packages/contracts) +win. -That leads to three explicit boundaries: +## The single first task -1. `apps/betternascontrolplane/` is a thin shell inside Nextcloud. -2. `exapps/control-plane/` owns betterNAS business logic and internal APIs. -3. `packages/contracts/` defines the interface between the shell app and the control plane. +Before splitting work across agents, do one foundation task: -## Why This Boundary Exists +- scaffold the four product parts +- lock the shared contracts +- define one end-to-end verification loop +- enforce clear ownership boundaries -Forking `nextcloud/server` would force betterNAS to own upstream patching and compatibility work too early. Pushing betterNAS logic into a traditional Nextcloud app would make the product harder to evolve outside the PHP monolith. The scaffold in this repository is designed to avoid both traps. +That first task should leave the repo in a state where later work can be +parallelized without interface drift. -## Responsibilities +## The four parts -### Nextcloud shell app +```text + betterNAS canonical contract -The shell app is responsible for: -- navigation entries -- branded entry pages inside Nextcloud -- admin-facing integration surfaces -- adapter calls into the betterNAS control plane + [2] control plane + +-----------------------------------+ + | system of record | + | users / devices / nodes / grants | + | mount profiles / cloud profiles | + +---------+---------------+---------+ + | | + control/API | | cloud adapter + v v + [1] NAS node [4] cloud / web layer + +-------------------+ +----------------------+ + | WebDAV + node | | Nextcloud adapter | + | real file bytes | | browser / mobile | + +---------+---------+ +----------+-----------+ + | ^ + | mount profile | + v | + [3] local device --------------+ + +----------------------+ + | Finder mount/helper | + | native user entry | + +----------------------+ +``` -The shell app is not responsible for: -- storage policy rules -- orchestration logic -- betterNAS-native RBAC decisions -- product workflows that may later be reused by desktop, iOS, or standalone web clients +## Non-negotiable rules -### Control-plane service +1. The control plane is the system of record. +2. File bytes should flow as directly as possible between the NAS node and the + local device. +3. The control plane should issue policy, grants, and profiles. It should not + become the default file proxy. +4. The NAS node should serve WebDAV directly whenever possible. +5. The local device consumes mount profiles. It does not hardcode infra details. +6. The cloud/web layer is optional and secondary. Nextcloud is an adapter, not + the product center. -The control plane is responsible for: -- domain logic -- policy decisions -- internal APIs consumed by betterNAS surfaces -- Nextcloud integration adapters kept at the service boundary +## Canonical sources of truth -### Shared contracts +Use these in this order: -Contracts live in `packages/contracts/` so request and response shapes do not get duplicated between PHP and TypeScript codebases. +1. [`docs/architecture.md`](/home/rathi/Documents/GitHub/betterNAS/docs/architecture.md) + for boundaries, ownership, and delivery rules +2. [`packages/contracts`](/home/rathi/Documents/GitHub/betterNAS/packages/contracts) + for machine-readable types, schemas, and route constants +3. the part docs for local detail: + - [`docs/01-nas-node.md`](/home/rathi/Documents/GitHub/betterNAS/docs/01-nas-node.md) + - [`docs/02-control-plane.md`](/home/rathi/Documents/GitHub/betterNAS/docs/02-control-plane.md) + - [`docs/03-local-device.md`](/home/rathi/Documents/GitHub/betterNAS/docs/03-local-device.md) + - [`docs/04-cloud-web-layer.md`](/home/rathi/Documents/GitHub/betterNAS/docs/04-cloud-web-layer.md) + - [`docs/05-build-plan.md`](/home/rathi/Documents/GitHub/betterNAS/docs/05-build-plan.md) -## Local Runtime +## The contract surface we need first -The local development stack uses Docker Compose so developers can bring up: -- Nextcloud -- PostgreSQL -- Redis -- the betterNAS control-plane service +The first shared contract set should cover only the seams that let all four +parts exist at once. -The Nextcloud shell app is mounted as a custom app and enabled through `./scripts/dev-up`. +### NAS node -> control plane +- node registration +- node heartbeat +- export inventory + +### Local device -> control plane + +- list allowed exports +- issue mount profile + +### Cloud/web layer -> control plane + +- issue cloud profile +- read export metadata + +### Control plane internal + +- health +- version +- the first domain entities: + - `NasNode` + - `StorageExport` + - `AccessGrant` + - `MountProfile` + - `CloudProfile` + +## Parallel work boundaries + +Each area gets an owner and a narrow write surface. + +| Part | Owns | May read | Must not own | +|---|---|---|---| +| NAS node | node runtime, export reporting, WebDAV config | contracts, control-plane docs | product policy | +| Control plane | domain model, grants, profile issuance, registry | everything | direct file serving by default | +| Local device | mount UX, helper flows, credential handling | contracts, control-plane docs | access policy | +| Cloud/web layer | Nextcloud adapter, browser/mobile integration | contracts, control-plane docs | source of truth | + +The only shared write surface across teams should be: + +- [`packages/contracts`](/home/rathi/Documents/GitHub/betterNAS/packages/contracts) +- this file when the architecture contract changes + +## Verification loop + +This is the first loop every scaffold and agent should target. + +```text +[1] mock or real NAS node exposes a WebDAV export +-> [2] control plane registers the node and export +-> [3] local device asks for a mount profile +-> [3] local device receives a WebDAV mount URL +-> user can mount the export in Finder +-> [4] optional cloud/web layer can expose the same export in cloud mode +``` + +If a task does not help one of those steps become real, it is probably too +early. + +## Definition of done for the foundation scaffold + +The initial scaffold is complete when: + +- all four parts have a documented entry point +- the control plane can represent nodes, exports, grants, and profiles +- the contracts package exports the first shared shapes and schemas +- local verification can prove the mount-profile loop end to end +- future agents can work inside one part without inventing new interfaces + +## Rules for future tasks and agents + +1. No part may invent private request or response shapes for shared flows. +2. Contract changes must update + [`packages/contracts`](/home/rathi/Documents/GitHub/betterNAS/packages/contracts) + first. +3. Architecture changes must update this file in the same change. +4. Additive contract changes are preferred over breaking ones. +5. New tasks should target one part at a time unless they are explicitly + contract tasks. diff --git a/packages/contracts/README.md b/packages/contracts/README.md new file mode 100644 index 0000000..fe0081a --- /dev/null +++ b/packages/contracts/README.md @@ -0,0 +1,42 @@ +# `@betternas/contracts` + +This package is the machine-readable source of truth for shared interfaces in +betterNAS. + +Use it to keep the four product parts aligned: + +- NAS node +- control plane +- local device +- cloud/web layer + +## What belongs here + +- shared TypeScript types +- route constants +- JSON schemas for payloads we want to validate outside TypeScript + +## What does not belong here + +- business logic +- per-service config +- implementation-specific helpers + +## Current contract layers + +- [`src/control-plane.ts`](/home/rathi/Documents/GitHub/betterNAS/packages/contracts/src/control-plane.ts) + - current runtime scaffold for health and version +- [`src/foundation.ts`](/home/rathi/Documents/GitHub/betterNAS/packages/contracts/src/foundation.ts) + - first product-level entities and route constants for node, mount, and cloud flows +- [`schemas/`](/home/rathi/Documents/GitHub/betterNAS/packages/contracts/schemas) + - JSON schema mirrors for the first shared entities + +## Change rules + +1. Shared API shape changes happen here first. +2. If the boundary changes, also update + [`docs/architecture.md`](/home/rathi/Documents/GitHub/betterNAS/docs/architecture.md). +3. Prefer additive changes until all four parts are live. +4. Do not put Nextcloud-only assumptions into the core contracts unless the + field is explicitly part of the cloud adapter. +5. Keep the first version narrow. Over-modeling early is another form of drift. diff --git a/packages/contracts/schemas/cloud-profile.schema.json b/packages/contracts/schemas/cloud-profile.schema.json new file mode 100644 index 0000000..3408a3d --- /dev/null +++ b/packages/contracts/schemas/cloud-profile.schema.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://betternas.local/schemas/cloud-profile.schema.json", + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "exportId", + "provider", + "baseUrl", + "path" + ], + "properties": { + "id": { + "type": "string" + }, + "exportId": { + "type": "string" + }, + "provider": { + "const": "nextcloud" + }, + "baseUrl": { + "type": "string" + }, + "path": { + "type": "string" + } + } +} diff --git a/packages/contracts/schemas/mount-profile.schema.json b/packages/contracts/schemas/mount-profile.schema.json new file mode 100644 index 0000000..c11b727 --- /dev/null +++ b/packages/contracts/schemas/mount-profile.schema.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://betternas.local/schemas/mount-profile.schema.json", + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "exportId", + "protocol", + "displayName", + "mountUrl", + "readonly", + "credentialMode" + ], + "properties": { + "id": { + "type": "string" + }, + "exportId": { + "type": "string" + }, + "protocol": { + "const": "webdav" + }, + "displayName": { + "type": "string" + }, + "mountUrl": { + "type": "string" + }, + "readonly": { + "type": "boolean" + }, + "credentialMode": { + "enum": [ + "session-token", + "app-password" + ] + } + } +} diff --git a/packages/contracts/schemas/nas-node.schema.json b/packages/contracts/schemas/nas-node.schema.json new file mode 100644 index 0000000..321e845 --- /dev/null +++ b/packages/contracts/schemas/nas-node.schema.json @@ -0,0 +1,52 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://betternas.local/schemas/nas-node.schema.json", + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "machineId", + "displayName", + "agentVersion", + "status", + "lastSeenAt", + "directAddress", + "relayAddress" + ], + "properties": { + "id": { + "type": "string" + }, + "machineId": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "agentVersion": { + "type": "string" + }, + "status": { + "enum": [ + "online", + "offline", + "degraded" + ] + }, + "lastSeenAt": { + "type": "string" + }, + "directAddress": { + "type": [ + "string", + "null" + ] + }, + "relayAddress": { + "type": [ + "string", + "null" + ] + } + } +} diff --git a/packages/contracts/schemas/storage-export.schema.json b/packages/contracts/schemas/storage-export.schema.json new file mode 100644 index 0000000..baa39c7 --- /dev/null +++ b/packages/contracts/schemas/storage-export.schema.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://betternas.local/schemas/storage-export.schema.json", + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "nasNodeId", + "label", + "path", + "protocols", + "capacityBytes", + "tags" + ], + "properties": { + "id": { + "type": "string" + }, + "nasNodeId": { + "type": "string" + }, + "label": { + "type": "string" + }, + "path": { + "type": "string" + }, + "protocols": { + "type": "array", + "items": { + "const": "webdav" + } + }, + "capacityBytes": { + "type": [ + "number", + "null" + ] + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + } +} diff --git a/packages/contracts/src/foundation.ts b/packages/contracts/src/foundation.ts new file mode 100644 index 0000000..1cb5f51 --- /dev/null +++ b/packages/contracts/src/foundation.ts @@ -0,0 +1,97 @@ +export const FOUNDATION_API_ROUTES = { + registerNode: "/api/v1/nodes/register", + nodeHeartbeat: "/api/v1/nodes/:nodeId/heartbeat", + listExports: "/api/v1/exports", + issueMountProfile: "/api/v1/mount-profiles/issue", + issueCloudProfile: "/api/v1/cloud-profiles/issue" +} as const; + +export type NasNodeStatus = "online" | "offline" | "degraded"; +export type StorageAccessProtocol = "webdav"; +export type AccessMode = "mount" | "cloud"; +export type AccessPrincipalType = "user" | "device"; +export type MountCredentialMode = "session-token" | "app-password"; +export type CloudProvider = "nextcloud"; + +export interface NasNode { + id: string; + machineId: string; + displayName: string; + agentVersion: string; + status: NasNodeStatus; + lastSeenAt: string; + directAddress: string | null; + relayAddress: string | null; +} + +export interface StorageExport { + id: string; + nasNodeId: string; + label: string; + path: string; + protocols: StorageAccessProtocol[]; + capacityBytes: number | null; + tags: string[]; +} + +export interface AccessGrant { + id: string; + exportId: string; + principalType: AccessPrincipalType; + principalId: string; + modes: AccessMode[]; + readonly: boolean; +} + +export interface MountProfile { + id: string; + exportId: string; + protocol: "webdav"; + displayName: string; + mountUrl: string; + readonly: boolean; + credentialMode: MountCredentialMode; +} + +export interface CloudProfile { + id: string; + exportId: string; + provider: CloudProvider; + baseUrl: string; + path: string; +} + +export interface StorageExportInput { + label: string; + path: string; + protocols: StorageAccessProtocol[]; + capacityBytes: number | null; + tags: string[]; +} + +export interface NodeRegistrationRequest { + machineId: string; + displayName: string; + agentVersion: string; + directAddress: string | null; + relayAddress: string | null; + exports: StorageExportInput[]; +} + +export interface NodeHeartbeatRequest { + nodeId: string; + status: NasNodeStatus; + lastSeenAt: string; +} + +export interface MountProfileRequest { + userId: string; + deviceId: string; + exportId: string; +} + +export interface CloudProfileRequest { + userId: string; + exportId: string; + provider: CloudProvider; +} diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index adb2a9c..8bbe6e3 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -1,2 +1,2 @@ export * from "./control-plane.js"; - +export * from "./foundation.js";