skeleton schemas

This commit is contained in:
Harivansh Rathi 2026-04-01 03:11:43 +00:00
parent 0032487ca1
commit 4f174ec3a8
9 changed files with 470 additions and 42 deletions

View file

@ -2,10 +2,23 @@
<img width="723" height="354" alt="image" src="https://github.com/user-attachments/assets/4e64fa91-315b-4a31-b191-d54ed1862ff7" />
## 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.

View file

@ -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.

View file

@ -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.

View file

@ -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"
}
}
}

View file

@ -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"
]
}
}
}

View file

@ -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"
]
}
}
}

View file

@ -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"
}
}
}
}

View file

@ -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;
}

View file

@ -1,2 +1,2 @@
export * from "./control-plane.js";
export * from "./foundation.js";