# Acequia System Architecture

Acequia is a peer-to-peer communication framework that enables real-time data exchange between browsers. Named after the traditional irrigation channels of the American Southwest, Acequia routes data between peers using multiple fallback transport strategies.

**Key design principle:** Everything looks like a web request from the outside. Apps register HTTP-style route handlers. The service worker intercepts requests and chooses the best transport: local postMessage between tabs, WebRTC data channels between browsers, or HTTP proxy via the discovery server.

## Three-Server Architecture

The Acequia system runs three services behind a single domain:

```
Browser
  ├─ Service Worker (intercepts all requests)
  │     ├─ /groups/* → route to peer handler (postMessage / WebRTC / HTTP proxy)
  │     ├─ /acq/*   → virtual filesystem (WebDAV mount points)
  │     └─ /*       → network fetch (file serving)
  │
  └─ Cloudflare Tunnel (HTTPS)
        │
        ├─ /(ws|groups/.*|peers\.html.*|admin/.*)
        │     → localDiscovery (port 31313)
        │       WebSocket signaling, route registry, HTTP-to-WS proxy
        │
        └─ everything else
              → localWebDAV (port 3333)
                File serving, authentication, WebDAV operations
```

### localWebDAV (port 3333)

The file server. Serves app code, static assets, and data files via WebDAV. Handles all authentication (JWT tokens, stored token IDs, user registration, invitations).

- **Technology:** Express + Nephele WebDAV adapter
- **Auth:** JWT tokens (PS256), stored token IDs, cookie/header/query parameter
- **Files:** Served from `acequia/{subdomain}/` directory
- **Config:** `config.mjs` (server), `public/config.js` (client)

### localDiscovery (port 31313)

The signaling server. Manages WebSocket connections for peer discovery, route registration, and request proxying. No persistent storage — all state is in-memory.

- **Technology:** Express + ws (WebSocket)
- **Protocol:** JSON messages over WebSocket at `/ws?group=groupId`
- **Features:** Peer registration, route registry with versioning, HTTP-to-WS request proxy, heartbeat/stale peer reaping

### Cloudflare Tunnel

Routes requests from `{subdomain}.acequia.live` to the correct local service based on URL path regex. Provides HTTPS (required for `getUserMedia` and service worker).

- **Ingress rule:** `/(ws|groups/.*|peers\.html.*|admin/.*)` → discovery server
- **Default:** everything else → WebDAV server

## Core Concepts

### Groups

A **group** is a collection of peers that can discover and communicate with each other. Each browser tab joins a group via WebSocket to the discovery server.

```javascript
const group = new acequia.groups.Group('camera', {
    displayName: 'My Camera',
    capabilities: ['camera', 'settings'],
})
await group.ready
```

Groups provide:
- Peer discovery (who else is in this group?)
- Route registration (what endpoints does each peer expose?)
- WebRTC signaling (SDP/ICE relay for establishing peer connections)
- Request proxying (forward HTTP requests to the right peer)

### Routes

A **route** is a URL pattern with a handler function. Apps register routes to expose functionality that other peers or external clients can call.

```javascript
group.registerRoute('/status', async (request) => {
    return {
        status: 200,
        body: JSON.stringify({ online: true }),
        headers: { 'content-type': 'application/json' },
    }
})
```

Routes are registered with the discovery server, which broadcasts the registry to all peers and the service worker. When a request arrives for `/groups/{groupId}/{route}`, the SW routes it to the correct handler.

### Instances

An **instance** is a single browser tab or window. Each instance has a unique `instanceId` (per session) and a persistent `deviceId`. The service worker tracks which instances are alive and which routes they own.

### Transport Fallback

When the SW receives a request for a group route, it tries transports in order:

1. **Local postMessage** — If the route handler is in a tab controlled by this SW, send via `client.postMessage()`. Fastest, same-browser only.
2. **WebRTC data channel** — If the route handler is in a remote browser, relay via an established peer connection. Fast, direct, requires WebRTC setup.
3. **HTTP proxy** — Send the request to the discovery server via HTTP, which forwards it over WebSocket to the target peer. Slowest but always works if the peer is connected.

### Service Worker Fetch Chain

The SW processes every fetch through a handler chain. First match wins:

```
1. handleGroupRouteRequest  → /groups/{groupId}/{instanceId}/...
   Match route pattern → find handler → postMessage or WebRTC or HTTP proxy

2. handleWebDAVRequest      → /acq/...
   Look up mount point in IndexedDB → rewrite URL → proxy to WebDAV server

3. alwaysFetchHandler       → URLs matching config.alwaysFetch patterns
   Always fetch from network (bypass cache)

4. cacheHandler             → everything else
   Serve from cache, or fetch and cache
```

### Route Registry Reliability

The route registry uses versioning to handle out-of-order updates and server restarts:

- Each route registration bumps a **version counter** on the discovery server
- The server includes a **sessionId** that changes on restart
- The SW tracks `{ version, sessionId }` per group
- If `sessionId` changes → server restarted, accept all updates
- If `version < current` with same `sessionId` → stale, reject
- On WebSocket reconnect, all routes are re-registered in a single batch message

## Identity & Authentication

### Device and User IDs

| ID | Scope | Storage | Purpose |
|----|-------|---------|---------|
| `deviceId` | Persistent | IndexedDB | Unique per browser profile |
| `userId` | Persistent | IndexedDB | Unique per user |
| `instanceId` | Session | sessionStorage | Unique per tab/window |

### Cryptographic Keys

Each device generates two PS256 key pairs on first run:
- **Device keys** — For signing device tokens
- **User keys** — For per-device user identity (device linking)

Public keys are registered with the auth server. Tokens are JWTs signed with the private key and verified using the registered public key.

### Token Flow

1. Browser generates JWT signed with device/user private key
2. JWT can be stored server-side as a **stored token** with a short ID (e.g., `z0lx3ktec7a8f3smvxkhodst`)
3. Token passed via: cookie (`auth_token`), header (`Authorization: Bearer`), or query param (`?token=`)
4. Server resolves stored token ID → JWT → verifies signature → extracts user/device

### User Roles

| Role | Read | Write | Manage Users |
|------|------|-------|-------------|
| `owner` | All paths | All paths | Yes |
| `editor` | Granted paths | Granted paths | No |
| `viewer` | Granted paths | No | No |
| `pending` | No | No | No |

## Configuration

### Server Config (`localWebDAV/config.mjs`)

```javascript
{
    useSubdomain: true,           // Extract subdomain from Host header
    mountPoints: {
        '/': {
            sourceRef: './acequia', // Filesystem root
            type: 'fs',
            auth: { type: 'jwt' },
        },
    },
}
```

### Client Config (`localWebDAV/public/config.js`)

```javascript
{
    debug: true,
    dataVersion: 'v0019.9999',
    cacheSource: false,
    acequia: {
        authURL: '/auth',
        discoveryURL: '',          // Empty = same origin
    },
    alwaysFetch: ['/'],           // Always fetch fresh from network
}
```

`discoveryURL: ''` means the discovery server is on the same origin (routed by the Cloudflare tunnel). The SW converts this to a WebSocket URL automatically.

## File Layout

### App Files

Apps live in `acequia/{subdomain}/AppName/`:

```
acequia/stigmergic/
├── Camera/
│   ├── index.html
│   └── src/
│       ├── main.js
│       ├── components/
│       ├── modules/
│       │   └── acequia-integration.js
│       ├── styles/
│       └── utils/
├── Scribbler/
│   ├── index.html
│   └── src/
│       └── ...
```

### Framework Files

The Acequia framework code:

```
acequia2/
├── acequia.js              # Main entry (exports full API)
├── sw.js                   # Service worker
├── config.js               # Default config
├── deps.js                 # Runtime-aware dependency loader
├── acequia/
│   ├── acequia.js          # Core initialization & identity
│   ├── groups.js           # Group class (WebSocket, routes)
│   ├── routes.js           # Router (pattern matching)
│   ├── errors.js           # HTTP error classes
│   ├── evented.js          # Event emitter base
│   └── webrtc/
│       ├── webrtc.js       # Peer lifecycle & signaling
│       ├── PeerBinary.js   # Binary data chunking
│       └── dataUtils.js    # Chunk/encode utilities
├── auth/
│   ├── auth.js             # Device registration
│   ├── tokens.js           # JWT creation & verification
│   └── deviceLink.js       # Device linking
└── build/
    └── acequia.js          # Bundled (~545 KB)
```

### Server Files

```
localWebDAV/
├── server.mjs              # Main WebDAV server
├── config.mjs              # Server config
├── public/
│   ├── config.js           # Client config
│   └── config.local.js     # Local overrides
├── src/auth/               # Auth handlers
└── acequia/                # Served files root

localDiscovery/
├── server.mjs              # Main discovery server
├── src/
│   ├── groups.mjs          # Group & route management
│   ├── peerProxy.mjs       # HTTP-to-WS proxy
│   ├── requests.mjs        # Request queue
│   └── errors.mjs          # Error classes
```

## Debugging

### Admin Endpoints

```bash
# Health check
curl http://localhost:31313/admin/health

# Route registry for a group
curl http://localhost:31313/admin/routes/camera

# All clients in a group
curl http://localhost:31313/admin/clients/camera

# Combined state
curl http://localhost:31313/admin/state/camera
```

### Browser DevTools

```javascript
// Get full SW state (routes, instances, versions)
const state = await acequia.diagnoseRoutes()

// Check peers
const peers = acequia.webrtc.getPeers()
```

### Service Worker

Check Application > Service Workers in DevTools. The SW logs route matches, transport decisions, and registry updates when `config.debug = true`.