Reference
Access control
Every site has an accessMode. Pick one at publish time, or change it
later with PATCH.
The four modes
| Mode | Who can view | Use it for |
|---|---|---|
public | Anyone with the URL | Marketing, demos, anything you'd link from Twitter. The default. |
private | Only you (signed in to the dashboard) | Drafts, internal reports, agent scratch space. |
password | Anyone with the URL and the password | Client previews, gated launches. |
restricted | You, plus people you grant by email, domain, or team | Sharing with named reviewers, a whole company domain, or a standing group. |
Setting the mode at publish time
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:
# 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:
Set-Cookie: df_access_<slug>=<jwt>; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400The 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:
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:
# 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.
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
accessModefrompasswordtopublicdoes 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.