Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Modellen Tools & Services
Oplossingen
Organisaties Ontwikkelaars Internet Service Providers Managed Service Providers AI-in-a-Box
Kenniscentrum
Ondersteuning Documentation Blog Downloads
Bedrijf
Over ons Onderzoek Vacatures Investeren Contact
Inloggen

Scratch storage & public links

Small text-shaped clips live inline in the envelope. That's cheap, fast, and requires nothing from the server beyond opaque bytes.

Big content is different. A 200 MB video, a PDF, a photo — inline in the envelope would bloat the log, slow fan-out, and fail entirely if the origin device logs off before every recipient catches up.

Scratch storage is where big content goes. Public share links are what you do with it when you want to hand something to someone who isn't on your ScaiGrid.

Both preserve end-to-end encryption. The server holds ciphertext and opaque metadata only.

When to reach for scratch#

  • The content is over the inline threshold (default 256 KiB after encryption).
  • The receiver's device might not be online at publish time; they'll pick it up later.
  • The origin device might go offline before every receiver has downloaded.
  • You want the clip to survive across a device swap.

For clips that don't need any of the above — a copy-paste of text between your own devices, right now — inline is still the right choice.

How it works#

Client encrypts locally (same primitives as inline clips — XChaCha20-Poly1305 for content, HPKE for wraps). Uploads ciphertext directly to Garage S3 via a signed URL. Publishes a small envelope carrying a MediaRef pointer to the scratch object. Recipients decrypt after fetching the ciphertext from Garage.

gdscript
1
2
3
4
5
6
client  grid   scratch.reserve  (size, sha256, mime_hint)
grid  client   { object_id, upload_url }
client  S3     PUT ciphertext directly
client  grid   scratch.commit   (object_id, wraps for each recipient device)
grid            HEADs S3 to verify hash + size, installs wraps
client publish  envelope carrying MediaRef{ref_scratch, object_id, sha256, size}

Reader side:

gdscript
1
2
3
4
5
receiver  grid  clip.new  (envelope with MediaRef pointer)
receiver  grid  scratch.get { object_id }
grid  receiver  { download_url, my_wrap }
receiver  S3    GET ciphertext
receiver         unwrap key with own priv, decrypt, done

Grid never handles payload bytes. Latency is one round-trip to grid for the URL + a direct S3 fetch.

Quotas#

Scratch storage is metered. Three levels, most-restrictive wins:

Level Default limit
Per user 5 GiB
Per tenant 100 GiB
Per partner 1 TiB

Object count is also capped (per user: 10 000) so a single user can't fragment the store with millions of tiny objects.

Uploads over quota get rejected at scratch.reserve with E_QUOTA_EXCEEDED { scope, used_bytes, limit_bytes, oldest_reclaimable }. The oldest_reclaimable hint lets your UI offer "You're out of scratch space. Delete this old file to upload?" without a separate list call.

Soft warning at 80% capacity. The scratch.reserve response carries quota_warning for that case — your client can prompt before uploads land users in the reject zone.

Per-tenant quota overrides for enterprise deployments are wired via tenant policy in a later drop.

Retention#

Two paths reclaim scratch storage:

  • Idle — objects untouched for 30 days (default) get reclaimed.
  • Absolute — hard cap of 90 days regardless of touch, catches "I uploaded it once and never checked back."

Both are configurable per-deployment. Downloads refresh the idle timer.

Deleted-explicitly objects (via scratch.delete) go immediately.

Public sharing#

Any scratch object can be minted into a public URL. This is how you hand a big file to someone who isn't on your ScaiGrid — a customer, a contractor, a friend. WeTransfer without the trust ask.

The URL shape#

scdoc
1
https://scaigrid.scailabs.ai/p/<token>#<b64url_key>
  • <token> is a random 128-bit ID. The server sees this.
  • <b64url_key> is the AEAD key that unlocks the ciphertext. Rides in the URL fragment — never sent to the server, never appears in logs. Only the browser sees it.

The server delivers ciphertext by token. The browser derives the key from the fragment and decrypts client-side. The server never handles plaintext, ever.

If you shared the URL over an untrusted channel — a customer's email, a Slack DM — an attacker who tapped the network sees /p/<token> in a proxy log, tries to fetch, and gets ciphertext they can't unlock without the fragment. That's the security posture Firefox Send was after, done right.

Optional password#

Pass password at publish time and the URL structure becomes:

scdoc
1
https://scaigrid.scailabs.ai/p/<token>#<b64url_kdf_salt>

Client-side: user enters password, Argon2id(password, salt) derives the key, decrypts. Server sees ciphertext + salt but never the password or the key.

Rate-limited login flow (server-side): 5 attempts per minute per token before the whole token is temporarily locked.

Download limits#

Cap the number of successful downloads at publish time — the link auto-expires after that many. Right primitive for one-time shares.

Revoke#

DELETE /v1/modules/scaiclip/scratch/{id}/public/{token} at any time. Returns 410-Gone on subsequent fetches.

Audit#

Every public event lands in audit_log:

  • scaiclip.public_link.created — with service_id if minted via SCSG
  • scaiclip.public_link.downloaded — includes IP hash, user-agent class, bytes_out
  • scaiclip.public_link.revoked
  • scaiclip.public_link.expired
  • scaiclip.public_link.password_failed — per-token attempt counter

Filter with the standard audit query API.

Anti-abuse#

Per-tenant rate limits on scratch.publish. Per-token rate limits on both fetch and password attempts. Enterprise deployments can plug in a scan-on-mint hook (call out to your own scanner) — it sees the client-supplied mime_hint and sha256 only, not plaintext, so scanning is opt-in and hash-based.

When to use ref_origin instead#

The other sharing mode — ref_origin — streams from the source device on demand, cached briefly by the grid. Use it when:

  • The content is ephemeral to that device (a screenshot you just took, a paste-buffer thumbnail).
  • You don't want it hitting central storage at all.
  • You don't need it to survive the origin going offline.

See MediaRef sharing modes for the mode comparison.

What's next#

Updated 2026-07-12 00:26:08 View source (.md) rev 6