Reference

Access control

Every site has an accessMode. Pick one at publish time, or change it later with PATCH.

The four modes

ModeWho can viewUse it for
publicAnyone with the URLMarketing, demos, anything you'd link from Twitter. The default.
privateOnly you (signed in to the dashboard)Drafts, internal reports, agent scratch space.
passwordAnyone with the URL and the passwordClient previews, gated launches.
restrictedYou, plus people you grant by email, domain, or teamSharing with named reviewers, a whole company domain, or a standing group.

Setting the mode at publish time

bash
curl -X POST https://dropfast.dev/api/v1/sites \
  -H "Authorization: Bearer df_sk_..." \
  -F "file=@./index.html" \
  -F "name=client-preview" \
  -F "accessMode=password" \
  -F "password=open-sesame"

If accessMode is password, the password field is required — the request returns 400 PASSWORD_REQUIRED otherwise.

Changing the mode later

PATCH /api/v1/sites/{slug} accepts JSON. You can change accessMode, the password, or both:

bash
# Take a public site private
curl -X PATCH https://dropfast.dev/api/v1/sites/my-slug \
  -H "Authorization: Bearer df_sk_..." \
  -H "content-type: application/json" \
  -d '{"accessMode":"private"}'
 
# Rotate the password
curl -X PATCH https://dropfast.dev/api/v1/sites/my-slug \
  -H "Authorization: Bearer df_sk_..." \
  -H "content-type: application/json" \
  -d '{"password":"new-password-here"}'

How password mode works

A visitor lands on /s/<slug>/ and sees the password gate (a minimal, inline-styled page — no JS, no React). They submit the password to POST /api/v1/sites/{slug}/verify-password. On success, the response sets an HttpOnly, SameSite=Lax cookie scoped to the slug:

http
Set-Cookie: df_access_<slug>=<jwt>; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400

The cookie lasts 24 hours. Subsequent requests to any path under the slug skip the gate until it expires.

The owner is never prompted. If you're signed in to the dashboard as the account that owns the site, visiting /s/<slug>/ serves the content directly — no password page, no cookie required. The password gate only applies to everyone else (signed-out visitors, or any other account).

Rate limiting

The verify-password endpoint is rate-limited at 5 attempts per minute per IP. Above the limit, the endpoint returns 429 RATE_LIMITED. The limiter is in-process — it resets after one minute.

Private sites

A private site returns 403 to anyone who isn't signed in as the owner, along with a small "This site is private — sign in as the owner" page. The slug's existence is visible (the response is 403, not 404), so private mode protects the contents but not the fact that a site at this slug exists.

This means private sites are appropriate for drafts you'd rather not leak, but they're not a secret-store. Don't put credentials in them, and don't rely on the slug itself being unguessable.

Reading a private site's contents from an agent

The browser origin (/s/<slug>/) gates a private site behind a sign-in wall a plain curl/webfetch can't clear. To read the bytes back programmatically, authenticate with your API key against the content endpoint — GET /api/v1/sites/{slug}/content/{path}, or dropfast fetch <slug> [path] from the CLI:

bash
curl https://dropfast.dev/api/v1/sites/my-slug/content/ \
  -H "Authorization: Bearer df_sk_..."

It authorizes off the key (not a browser cookie), so the owner reads any mode and a grantee reads a restricted site. Anything a caller may not read returns 404 — the endpoint never discloses a site it can't serve.

Restricted — share with people, domains, and teams

restricted mode opens a site to its owner plus a list of access grants. A grant names one of three kinds of principal:

  • email — a single person, e.g. alice@acme.com.
  • domain — everyone with a verified email at a domain, e.g. acme.com.
  • team — every member of a team you've granted.

Flip a site to restricted, then manage grants on the Share panel in the dashboard, or over the REST API:

bash
# Flip to restricted
curl -X PATCH https://dropfast.dev/api/v1/sites/my-slug \
  -H "Authorization: Bearer df_sk_..." \
  -H "content-type: application/json" \
  -d '{"accessMode":"restricted"}'
 
# Grant access to a person, a domain, or a team
curl -X POST https://dropfast.dev/api/v1/sites/my-slug/grants \
  -H "Authorization: Bearer df_sk_..." \
  -H "content-type: application/json" \
  -d '{"granteeType":"email","granteeValue":"alice@acme.com"}'
 
# List grants, then revoke one by id
curl https://dropfast.dev/api/v1/sites/my-slug/grants -H "Authorization: Bearer df_sk_..."
curl -X DELETE https://dropfast.dev/api/v1/sites/my-slug/grants/<grantId> -H "Authorization: Bearer df_sk_..."

Grant management is owner-only. A duplicate grant returns 409 GRANT_EXISTS; a malformed email/domain returns 400 INVALID_GRANTEE.

Sign in before you can view

A signed-out visitor to a restricted site is redirected to sign-in (/sign-in?redirect_url=…) and returns to the page afterward. This is how "share with someone who doesn't have an account yet" works: you grant their email, they sign up with it, and access appears the moment that email is verified — no extra step.

The access check runs first, ahead of any other handling of the URL — a link that arrives without its trailing slash still hits the wall before it is canonicalized, on every mode.

Verified email is the rule

Email and domain grants only match an address that Clerk has marked verified. An unverified address — even one in a granted domain — grants nothing. This is what stops someone signing up as ceo@acme.com (unverified) to read everything shared with @acme.com.

Domain grants are powerful: @acme.com is "anyone who can verify an @acme.com address." Granting a free-mail domain like @gmail.com shares with everyone on that provider — the dashboard warns you, but doesn't stop you. Prefer per-email grants when the audience is small.

Find what's shared with you

GET /api/v1/shared-with-me lists restricted sites you can reach through an email, domain, or team grant.

Teams

A team is a named group you can grant access to a set of sites. Anyone can create one and becomes its owner.

bash
curl -X POST https://dropfast.dev/api/v1/teams \
  -H "Authorization: Bearer df_sk_..." \
  -H "content-type: application/json" \
  -d '{"name":"Q3 reviewers"}'
 
# Add / remove members (any member may; owner-only for sensitive ops)
curl -X POST https://dropfast.dev/api/v1/teams/<id>/members \
  -H "Authorization: Bearer df_sk_..." -H "content-type: application/json" \
  -d '{"email":"teammate@acme.com"}'
curl -X DELETE "https://dropfast.dev/api/v1/teams/<id>/members?email=teammate@acme.com" \
  -H "Authorization: Bearer df_sk_..."

Membership rules — deliberately group-managed:

  • Only the owner can delete the team or transfer ownership.
  • Nobody can remove themselves. The owner leaves only by transferring ownership (POST /api/v1/teams/{id}/transfer) or deleting the team.
  • Any member can add a member, or remove another non-owner member.
  • The owner is identified by a stable account id, not by an email — so an email change can never hand control of a team to someone else.

Grant a site to a team from the site's Share panel (granteeType: "team", granteeValue: <teamId>). Deleting a team revokes every site grant made to it.

Caching

Public sites are cached at Vercel's edge for up to 24 hours, with a 7-day stale-while-revalidate window on top. Browsers re-check after 60 seconds, so a viewer who hard-reloads sees fresh content quickly, but a viewer who just clicks the link again may see the previous version of your site for up to a day. PUT /api/v1/sites/{slug} (re-upload) does not yet invalidate the edge cache — viewers can force a refresh with Cmd/Ctrl+Shift+R, or you can append a cache-busting query string to the link you share.

Private, password-protected, and restricted sites bypass the CDN entirely (Cache-Control: private, no-store), so changes to those — including adding or revoking a grant — propagate immediately.

Gotchas

  • Changing accessMode from password to public does not revoke existing cookies. Anyone whose cookie hasn't expired keeps access until it does (or you delete the site).
  • A password-protected site's bytes are still stored unencrypted in S3. Treat the password as a friction step, not as encryption.
  • Owner identity is enforced at request time by /s/<slug>/[...path] from the Clerk dashboard session — this drives both private-mode access and the password-mode owner bypass. If you're serving DropFast through your own reverse proxy, make sure it forwards the Clerk cookie; without it the owner is treated as an anonymous visitor and sees the gate.
  • Deleting a site automatically deactivates any aliases pointing at it. The alias rows stay in your account so you can repoint them; the /_/<alias> URL starts returning 404 until you do.
Edit this page on GitHub