Philosophy
The design principles behind the ScaiDrive API. Read this if you want to know why the API looks the way it does before you start integrating.
The server is the source of truth#
Every file has exactly one authoritative copy: the one on the server. Clients hold replicas. If a client and the server disagree, the server wins unless you explicitly say otherwise.
This means:
- Every mutation goes through the server. Clients don't peer-sync. A change made on Device A reaches Device B only after the server has accepted it.
- Version numbers come from the server. Clients don't generate them.
base_version_idin a sync-apply request tells the server "I'm editing on top of this version" — if the server has a newer version, you get a conflict. - Permissions are server-enforced. Client-side checks are advisory. The server re-checks every permission on every request.
This is the opposite of a peer-to-peer design, and intentional. A central source of truth is easier to audit, easier to back up, easier to reason about, and easier to integrate with enterprise compliance.
Permissions mirror NTFS#
ScaiDrive's permission model is Discretionary Access Control with inheritance — the same shape as Windows ACLs. Each file and folder can have its own ACL with allow and deny entries, inherited from the parent or broken. Share membership defines the baseline role; ACL entries refine it.
Why not a simpler model (owner + share + permissions)? Because the organizations that care about file sharing already understand NTFS. If your IT team's mental model is "inherited ACEs, inheritance break, deny over allow," ScaiDrive lets you apply it directly. See Permissions and ACLs.
Sync is a cursor, not a snapshot#
Classic sync systems scan the filesystem, compare to a prior snapshot, and compute a diff. Expensive on large directories, brittle under concurrent writes.
ScaiDrive inverts this: every mutation appends to a per-tenant change log, and clients track a cursor — an opaque position in that log. To sync, a client asks GET /api/v1/sync/changes?cursor=<last> and gets the ordered stream of changes since their checkpoint. No scans. No diffs. Just replay.
The cursor approach means:
- Incremental is the default. Full sync is just "start from cursor 0."
- Offline recovery is free. Reconnect after a week, replay a week of changes, done.
- Eventing is cheap. The same log powers the real-time WebSocket and the polling sync API.
See Sync Model.
Chunks, not files, are the storage unit#
Files are split into SHA-256-addressed chunks. The same chunk, used by a hundred files, is stored once. Uploads that duplicate existing content transfer zero bytes on the wire for the duplicated portion — the client sends the chunk hash, the server returns already have.
This means:
- Deduplication is automatic. No per-user dedup tables, no scheduled garbage runs.
- Deltas are free. Changing 2 MB of a 500 MB file means uploading 2 MB.
- Storage costs scale with unique content, not user count. If a PDF is attached to ten emails and synced to ten laptops, you store it once.
See Versioning and Deduplication.
Explicit over implicit#
When a decision has tradeoffs, ScaiDrive makes you state your choice.
- Delete is soft by default. Set
?permanent=truewhen you mean it. - Sync conflict resolution is a request parameter. No hidden "merge heuristic." You pass
conflict_resolution: LAST_WRITER_WINSorMANUAL. - External links specify every policy up front. Expiry, password, max downloads, IP allow-list, required-email — all in the
POSTbody, all visible in theGET. - ACLs don't inherit silently. Every ACE records whether it was
inherited: trueor explicitly set.
You should be able to read a ScaiDrive request and know exactly what will happen.
Responses are the resource, errors are the status code#
Successful responses return the resource directly — no wrapper envelope. A file metadata response is a file metadata object; a list response is {items: [...], total: N}. Whatever the endpoint promises in its schema, that's what comes back.
1 | |
Errors use the HTTP status code as the primary signal. The body is {"detail": "<message>"} for regular errors, or FastAPI's structured {"detail": [{"loc": [...], "msg": "...", "type": "..."}, ...]} for request-validation failures. Branch on the status code; surface the message in UIs.
1 | |
See Errors for the full status-code map and validation-error shape.
Batch operations exist where they save round trips#
Operations that naturally occur in bulk — permission checks, sync-change application, file deletion — take arrays. They don't take arrays just for the sake of it.
For example, POST /api/v1/permissions/check/batch accepts up to 100 checks in one request. POST /api/v1/sync/apply accepts up to 5,000 changes. Where the operation is genuinely per-resource (reading file metadata, creating a folder), we don't pretend it should be bulk.
What's next#
- Architecture — how these principles show up in the system design.
- Quickstart — see the API in action.
- Sync Model — the cursor design in detail.