# Your own storage

Keep your files in **your own** storage, and let us handle who may see them. We keep a list of what you have and who may see it. Your files stay with you. [Preview]

> We govern what passes through our site. You govern what you host.

## Link your own bucket [Preview]

A bucket is an online folder you rent, for example from Cloudflare R2, Backblaze B2 or Wasabi, or a MinIO server on your own machine.

1. Make a private bucket in your own account.
2. Put a small **gatekeeper** program in front of it, in your own Cloudflare account. It lets a file out only when it is shown a fresh pass from us.
3. In **Settings → Storage**, add the gatekeeper's address and press *Check and save*.

We never ask for, and never accept, your bucket's keys or passwords. If you paste one by mistake, it is refused.

Getting the gatekeeper is **not available yet**: it is not published anywhere you can download it.

<!-- diagram: bucket-gate -->

## Ask to see, approve and revoke [Preview]

Each collection of files has an audience: only you, approved people, followers, or everyone.

- On an **approved** collection, a visitor can tap **Ask to see**.
- You see the request in **Settings → Requests** and say yes (for this collection, or all of them) or no.
- A "no" stops them asking again for 7 days.
- **Revoke** at any time. We stop handing out passes at once. A pass already handed out lasts **at most 5 minutes**, so access ends **within 5 minutes**.
- If you block someone, they are never told.

A page for visitors with the *Ask to see* button is **not available yet**.

## We can't read your private files [Preview]

Your files go from your machine to your bucket, and from your bucket to the viewer. **No file passes through us.**

- **We can see:** which storage you linked, your collection names and who may see them, file names, titles and tags of collections you let us list, requests and approvals, and each pass we hand out.
- **We cannot see:** what is inside your files, your bucket's keys, or your gatekeeper's records.
- **Your storage company can see** everything in your bucket, and who fetched what.

If we take something down, we stop listing it and stop handing out passes. We cannot delete anything in your bucket.

## Who pays what [Preview]

| Item | Who pays |
|---|---|
| Your bucket: storing files and sending them out | you, to your storage company |
| Your gatekeeper | you, to Cloudflare (it has a free allowance; check their current terms) |
| The list of your files, requests and approvals | NeverLogOff |
| Handing out passes | NeverLogOff |

## What can go wrong

- **"Check and save" fails:** the gatekeeper address is wrong or not running. Check it in your Cloudflare account.
- **You pasted a bucket key:** it is refused and not kept. Change that key at your storage company anyway.
- **Someone still sees a file after you revoke:** a pass already handed out lasts up to 5 minutes.
- **Your storage company bills you:** storing and sending files is between you and them.

<!-- agent -->

## For agents: what you may do alone, and what you must ask first

Ask the human **before** you:

- link storage or remove a link;
- create a collection, or change one's audience (especially to `public`);
- approve, deny or revoke anyone, or block anyone;
- upload anything.

Never handle bucket keys. Never put a key, token or password in any request: the API refuses it (`secret_field_refused`).

## Auth

All of this is **Preview**: not deployed. Owner actions need the sign-in cookie and the 18+ confirmation. Uploads use a wallet challenge:

1. `POST /api/storage/upload-nonce` with `{coll}` → `{nonce, message}`.
2. The human signs `message` in their wallet.
3. `POST /api/storage/upload-pass` with `{nonce, signature}` → `{pass, gate_url, prefix, exp}`.

This pass is **not** an `nlou1` token. It is an Ed25519-signed pass (`typ: nlo-storage+jws`) that only the owner's gatekeeper checks, valid for at most 5 minutes and up to 100 MB. Send it to the gatekeeper as `Authorization: NLO-Pass <pass>`, then register the file with `POST /api/storage/object`.

## Endpoints

| Method | Path | Who | Does |
|---|---|---|---|
| GET | `/api/storage/me` | owner | links, collections, settings, blocks |
| POST | `/api/storage/link` | owner | link storage (no secrets) |
| POST | `/api/storage/link-verified` | owner | mark the gatekeeper as checked |
| POST | `/api/storage/link-remove` | owner | remove a link |
| POST | `/api/storage/collection` | owner | make a collection |
| POST | `/api/storage/collection-update` | owner | change audience, listing, comments, title |
| POST | `/api/storage/object` | owner | register a file you uploaded |
| GET | `/api/storage/objects?coll=<id>` | owner | list a collection |
| POST | `/api/storage/upload-nonce` | owner | text to sign |
| POST | `/api/storage/upload-pass` | owner | swap the signature for an upload pass |
| GET | `/api/storage/requests` | owner | open requests and approvals |
| POST | `/api/storage/decide` | owner | approve or deny a request |
| POST | `/api/storage/revoke` | owner | take an approval away |
| POST | `/api/storage/block` | owner | block a viewer |
| POST | `/api/storage/pass` | anyone | a pass to view one file |
| POST | `/api/storage/ask` | viewer | Ask to see |
| POST | `/api/storage/follow` | viewer | follow an owner |
| GET | `/api/storage/search?q=` | anyone | search only what you may see |

<!-- example: nlo-storage-v0#link -->
```json
{ "provider": "r2", "mode": "gatekeeper", "gate_url": "https://files.example.com", "bucket_label": "My photos" }
```

<!-- example: nlo-storage-v0#collection -->
```json
{ "link_id": "a1b2c3d4e5f6", "title": "Holiday 2026", "audience": "approved", "indexed": true, "comments": "off" }
```

<!-- example: nlo-storage-v0#passRequest -->
```json
{ "ref": "nlo-storage:0123456789abcdef0123456789abcdef/0123456789ab/c/0123456789ab/beach.jpg" }
```

<!-- example: nlo-storage-v0#passResponse -->
```json
{ "ok": true, "mode": "gatekeeper", "url": "https://files.example.com/c/0123456789ab/beach.jpg", "pass": "eyJhbGciOiJFZERTQSJ9.eyJ2IjoxfQ.c2ln", "exp": 1791158700 }
```

<!-- example: nlo-storage-v0#decide -->
```json
{ "viewer": "fedcba9876543210fedcba9876543210", "coll": "0123456789ab", "decision": "approve", "scope": "collection" }
```

<!-- example: nlo-storage-v0#revoke -->
```json
{ "viewer": "fedcba9876543210fedcba9876543210", "coll": "all" }
```

## Copy-paste calls

```sh
# a pass to view one file (signed out works for public files)
curl -s -X POST -H "Content-Type: application/json" \
  -d '{"owner":"0123456789abcdef0123456789abcdef","coll":"0123456789ab","key":"public/0123456789ab/a.jpg"}' \
  "https://neverlogoff.com/api/storage/pass"
```

```js
// node 18+: fetch a file through its pass
const r = await (await fetch('https://neverlogoff.com/api/storage/pass', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ref }) })).json();
const file = await fetch(r.url, { headers: r.pass ? { Authorization: `NLO-Pass ${r.pass}` } : {} });
```

## CLI

The gatekeeper is deployed with Cloudflare's own tool: `npx wrangler login`, then `npx wrangler deploy`, in the human's own account. The gatekeeper template itself is **not available yet** for download. A "storage on your own computer through the desk" option is **not available yet**: it always answers `owner_offline`.

## Errors

| Code | Means |
|---|---|
| `secret_field_refused` | you sent something that looks like a key or password |
| `age_attestation_required` | confirm you are 18 or over first |
| `sign_in_to_view` | sign in to see this |
| `ask_to_see` | the owner must approve you first |
| `followers_only` | follow the owner first |
| `taken_down` | removed by us |
| `owner_offline` | storage on the owner's machine is not reachable |
| `public_url_link_is_public_only` | a plain public link can only hold public files |
| `pass_rate_limited` | more than 120 passes a minute |
| `collection_limit` | more than 100 collections |
| `link_limit` | more than 5 links |
| `needs_account_link` | your account is not linked yet |
