# dela docs

dela publishes websites and files to live URLs in seconds — for people and for AI agents.
Every site gets its own address at `https://{slug}.dela.click`, e.g. `https://azure-harbor-j52c.dela.click`.

- **Base API URL:** `https://dela.click/api/v1`
- **Agent skill:** `https://dela.click/skill.md` · **LLM index:** `https://dela.click/llms.txt`
- **One-line install:** `curl -fsSL https://dela.click/install.sh | bash`

## Quick start (no account)

Anyone can publish without signing in. Anonymous sites are **deleted 48 hours after creation unless claimed**.

```bash
curl -fsSL https://dela.click/publish.sh -o publish.sh && chmod +x publish.sh
./publish.sh ./my-site          # prints the live URL and a claim link
```

The response includes a `claimUrl` — open it, sign in with your email, and the site is yours permanently.

## Authentication

Sign-in is passwordless: we email a 6-digit code.

| Header | Meaning |
|---|---|
| `Authorization: Bearer <apiKey>` | Act as your account. Omit for anonymous publishing. |
| `X-Dela-Client: <agent>/<tool>` | Attribution, e.g. `claude-code/publish-sh`. Shown in the dashboard. |
| `X-Dela-Account: <workspace id or handle>` | Act in a workspace instead of your personal account. |

### Agent sign-in (get an API key)

1. Ask the user for their email.
2. `POST /api/auth/agent/request-code` `{"email": "you@example.com", "client": "claude-code"}`
3. Ask the user for the code from their inbox.
4. `POST /api/auth/agent/verify-code` `{"email": "...", "code": "123-456", "keyName": "claude-code on laptop"}`
   → `{"apiKey": "dela_…", "apiKeyId": "…", "isNewUser": false}`
5. Store the key (e.g. `~/.dela/credentials`, mode 600). Keys are shown once; verifying again with the same `keyName` rotates it.

Manage keys in the dashboard (**Settings → API keys**) or with `GET/POST /api/v1/me/keys`, `DELETE /api/v1/me/keys/:id`.

## Publishing: create → upload → finalize

### 1. Create

`POST /api/v1/publish`

```json
{
  "files": [{ "path": "index.html", "size": 1234, "hash": "<sha256 hex>", "contentType": "text/html; charset=utf-8" }],
  "displayName": "Launch page",
  "displayDescription": "One-line summary for the dashboard",
  "spaMode": false,
  "crossOriginIsolation": false,
  "viewer": { "title": "…", "description": "…", "ogImagePath": "cover.png" },
  "ttlSeconds": null,
  "folder": "Reports"
}
```

Response (abridged):

```json
{
  "slug": "azure-harbor-j52c",
  "siteUrl": "https://azure-harbor-j52c.dela.click/",
  "upload": {
    "versionId": "01JC…",
    "uploads": [{ "path": "index.html", "method": "PUT", "url": "https://dela.click/api/v1/upload/…", "headers": { "content-type": "text/html; charset=utf-8" } }],
    "skipped": [],
    "finalizeUrl": "https://dela.click/api/v1/publish/azure-harbor-j52c/finalize",
    "expiresInSeconds": 3600
  },
  "claimToken": "…", "claimUrl": "https://dela.click/c/azure-harbor-j52c/…", "expiresAt": "…"
}
```

`claimToken` / `claimUrl` only appear for anonymous publishes and are **returned once**. Share the claim URL with the user exactly as returned.

### 2. Upload

`PUT` each file's bytes to its `url` with the given headers. Uploads can run in parallel. The server verifies the SHA-256.
Files whose hash the site already has are listed in `skipped` and need no upload. Upload URLs expire after an hour —
refresh with `POST /api/v1/publish/:slug/uploads/refresh` `{"versionId"}`.

### 3. Finalize

`POST /api/v1/publish/:slug/finalize` `{"versionId": "…"}` → the version goes live immediately.

- Retrying finalize is safe (`replayed: true`). Publishing identical content returns `unchanged: true`.
- `warnings[]` reports problems such as an invalid `.dela/data.json`.
- Anonymous sites: include `"claimToken"` in the body.

### Updating a site

`PUT /api/v1/publish/:slug` with the same body as create (owner API key, or `claimToken` for anonymous sites), then upload and finalize.
Pass `"baseVersionId"` to fail with `409 version_conflict` if someone else (or the dashboard editor) published in the meantime.

### Limits

| | Anonymous | Account |
|---|---|---|
| Lifetime | 48 hours unless claimed | Permanent (or `ttlSeconds`) |
| Max file size | 100 MB | 100 MB |
| Files per version | 2,500 | 2,500 |
| Version size | 250 MB | 1 GB |
| Storage | — | 10 GB per account |
| Sites | — | 500 per account |
| Publish rate | 20 per hour per IP | 120 per hour; 60 versions per site per hour |

## How sites are served

- `index.html` at the root (or in a folder) is served for directory URLs; `/about` also serves `about.html`.
- Without an `index.html`, a site's only top-level HTML file (e.g. `report.html`) is served at `/`.
- A site with a single non-HTML file gets a full-page viewer: images (including SVG), video, audio and PDF are embedded, Markdown is rendered, other text is shown as-is, and anything else gets a download page. Set `viewer.title`/`description` to label it.
- Folders without an `index.html` show a file listing.
- A root `404.html` is used for missing pages. With `spaMode: true`, unknown paths serve `index.html`.
- `foo.js.br` / `foo.js.gz` are served precompressed when the browser accepts them. Range requests work for media.
- `crossOriginIsolation: true` adds COOP/COEP headers (for `SharedArrayBuffer` / WebAssembly threads).
- Files under `.dela/` are configuration and are never served.

## Sites API

| Method | Path | |
|---|---|---|
| GET | `/api/v1/sites` | List your sites (`?folder=`, `?scope=all`) |
| GET | `/api/v1/sites/search?q=` | Full-text search across names, paths and content |
| GET | `/api/v1/sites/:slug` | Details |
| PATCH | `/api/v1/sites/:slug` | `displayName`, `displayDescription`, `ttlSeconds`, `spaMode`, `crossOriginIsolation`, `viewer`, `password`, `folder`, `onProfile`, `pinned` |
| DELETE | `/api/v1/sites/:slug` | Delete the site, all versions and data |
| POST | `/api/v1/sites/:slug/duplicate` | Copy into a new site |
| GET | `/api/v1/sites/:slug/files` | Manifest of the live version |
| GET | `/api/v1/sites/:slug/files/{path}` | Download a file (works for private sites) |
| POST | `/api/v1/sites/:slug/claim` | `{"claimToken"}` — claim an anonymous site |

### Versions

Every publish is an immutable version.
`GET /api/v1/sites/:slug/versions` · `POST …/versions/:versionId/restore` · `DELETE …/versions/:versionId`.
Each version has a private preview at `https://{versionId}--{slug}.dela.click` (owner only).

### Access control

`PATCH /api/v1/sites/:slug/access`

```json
{ "mode": "restricted", "allowedEmails": ["ada@example.com"], "allowedDomains": ["example.com"], "notify": true }
```

| mode | Who can view |
|---|---|
| `public` | Anyone with the link (default) |
| `password` | Set with `PATCH /api/v1/sites/:slug {"password": "…"}`; `null` removes it |
| `restricted` | Listed emails and domains (verified by email code). Empty lists = only you |
| `members` | Members of the owning workspace |

Lists are replaced, not merged. Changing access signs out existing viewers.

## Site Data

Give a static site a small database — forms, comments, waitlists, votes. Declare collections in `.dela/data.json`:

```json
{
  "collections": {
    "guestbook": {
      "fields": {
        "name": { "type": "string", "required": true, "maxLength": 80 },
        "message": { "type": "string", "required": true, "maxLength": 1000 }
      },
      "access": { "read": "public", "insert": "public", "update": "owner", "delete": "owner" },
      "rateLimit": "10/hour/ip"
    }
  }
}
```

Field types: `string`, `number`, `integer`, `boolean`, `url`, `email`, `datetime`, `array`, `object` with
`required`, `default`, `maxLength`, `minLength`, `minimum`, `maximum`, `maxItems`, `enum`. Access levels: `public`, `owner`, `none`.
Public update/delete additionally require `"publicMutation": "open"`.

From the page (relative URLs work on any domain):

```js
const { records, nextCursor } = await (await fetch("./.dela/data/guestbook?limit=50")).json();
await fetch("./.dela/data/guestbook", {
  method: "POST",
  headers: { "content-type": "application/json", "idempotency-key": crypto.randomUUID() },
  body: JSON.stringify({ name: "Ada", message: "Hello!" }),
});
```

Owner API: `GET|POST /api/v1/sites/:slug/data/:collection`, `GET|PATCH|DELETE /api/v1/sites/:slug/data/:collection/:id`.
Records belong to the site (not a version). Limits: 10 collections, 16 KB per record, 25,000 records per collection.
Site Data requires a claimed site.

## Proxy routes & service variables

Call third-party APIs from a static site without exposing keys. Store secrets in **Settings → Services**
(`PUT /api/v1/variables/:NAME {"value", "allowedUpstreams": ["api.example.com"]}`), then declare routes in `.dela/proxy.json`:

```json
{ "proxies": { "/api/chat": { "upstream": "https://api.example.com/v1/chat", "method": "POST", "headers": { "Authorization": "Bearer ${EXAMPLE_KEY}" }, "rateLimit": "20/hour/ip" } } }
```

`/prefix/*` routes forward the rest of the path. Only `content-type`/`accept` headers are forwarded from the browser; cookies never are.

## Analytics

`GET /api/v1/sites/:slug/analytics?range=24h|7d|30d|90d` → views, visitors, top paths, referrers, countries, crawlers, 404s and a time series.
`GET /api/v1/analytics?range=…` aggregates the whole account.

## Custom domains

`POST /api/v1/domains {"domain": "www.example.com"}` returns the DNS records to add (a CNAME to `fallback.dela.click`).
Then `PUT /api/v1/domains/:domain/site {"slug": "…", "primary": true}` — with `primary`, the dela address redirects to your domain.

## Folders, profile, workspaces

- Folders: `GET|POST /api/v1/folders`, `PATCH|DELETE /api/v1/folders/:id`; set a site's folder with `PATCH /api/v1/sites/:slug {"folder": "Name"}`.
- Public profile at `https://dela.click/@username` (+ `/feed.json`): `GET|PATCH /api/v1/profile`.
- Workspaces: `POST /api/v1/accounts {"displayName"}`, invite with `POST /api/v1/accounts/:id/invites {"emails"}`, publish into one with `X-Dela-Account`.

## Errors

```json
{ "error": "…", "code": "rate_limit_exceeded", "message": "…", "details": {}, "retry_after": 60, "docs_url": "https://dela.click/docs" }
```
