Skip to content

Navigation Menu

Sign in
Sign up

Repository files navigation

Cloudflare Email Collector

简体中文

Invite-only, multi-user email collection and content distribution built on Cloudflare Workers + Durable Objects + KV + D1.

It gives you a small hosted workflow for:

  • creating short public collection links
  • distributing codes / links / any prepared text, one item per visitor (claim links)
  • limiting each link by quota
  • auto-closing links exactly when full
  • managing multiple invited users from one admin panel
  • storing user/session data separately from submission state

Screenshots

Landing page Public collection page

Admin dashboard Member workspace

Demo

Why this exists

A plain KV-only approach is nice for storage, but it is a bad fit for real-time counters on Cloudflare. This project uses:

  • Durable Object for the authoritative invite state and submission count
  • KV for mirrored email archives
  • D1 for invited users and login sessions

That keeps the critical path consistent while still making export/archive cheap.


Features

  • Invite-only admin flow
    • /admin for the super admin
    • create member accounts
    • see latest generated credentials once
  • Multi-user workspace
    • /login and /app for invited members
    • each member manages only their own links
    • forced password change flow for first login
  • Short public links
    • public links use /:shortId
    • IDs start at 4 characters and grow only after collisions
    • reserved paths like /admin and /api stay protected
  • Quota-aware collection
    • link closes automatically when quota is reached
    • manual open/close and delete actions
  • Claim links (content distribution)
    • paste a batch of items (one per line): coupon codes, download links, passwords, anything
    • each visitor atomically claims exactly one unclaimed item
    • optional email gate: one claim per email, repeat visits show the same item
    • or fully anonymous one-click claiming
    • link auto-closes when every item is claimed
    • owner sees which email claimed which item
  • Consistent writes
    • submission count is handled inside a Durable Object
    • KV stores a mirrored copy of each normalized email submission
  • Minimal UI
    • single Worker app
    • admin, member workspace, and public page included

Architecture

Browser
 │
 ▼
Cloudflare Worker
 ├─ Admin + member auth routes
 ├─ Public invite pages
 └─ API routes
 │
 ├─ D1
 │ ├─ users
 │ └─ user_sessions
 │
 ├─ Durable Object: InviteRegistry
 │ ├─ invite config
 │ ├─ submission count
 │ └─ auto-close decision
 │
 └─ Workers KV
 └─ mirrored email archive

Storage split

Store Responsibility
D1 users, passwords, sessions
Durable Object authoritative invite state, quota checks, auto-close, submission registry
KV mirrored submission archive

Routes

UI

  • GET / — landing page
  • GET /admin — super admin login / dashboard
  • GET /login — member login
  • GET /app — member workspace
  • GET /:shortId — public collection page
  • GET /i/:inviteId — legacy redirect to /:shortId

Admin API

  • GET /api/admin/users
  • POST /api/admin/users
  • GET /api/admin/invites
  • POST /api/admin/invites/:inviteId/toggle
  • DELETE /api/admin/invites/:inviteId

Member API

  • GET /api/me/invites
  • POST /api/me/invites
  • POST /api/me/password
  • POST /api/me/invites/:inviteId/toggle
  • DELETE /api/me/invites/:inviteId

Public API

  • POST /api/invites/:inviteId/submit — collect mode
  • POST /api/invites/:inviteId/claim — claim mode
  • GET /healthz

Username and short ID rules

Usernames

  • lowercase letters, numbers, underscore
  • minimum length: 4
  • maximum length: 24

Examples:

  • valid: team_a, user01, mail_ops
  • invalid: ab, my-name, UpperCase

Public short IDs

  • alphabet removes ambiguous characters
  • starts at 4 chars
  • grows to 5/6 chars only when collisions happen

Quick start

1) Install

npm install
cp .dev.vars.example .dev.vars

Set local values in .dev.vars:

ADMIN_PASSWORD=replace-with-a-long-password
SESSION_SECRET=replace-with-a-long-random-secret

2) Create Cloudflare resources

Create a D1 database:

npx wrangler d1 create cloudflare-email-collector

Create a KV namespace:

npx wrangler kv namespace create EMAIL_COLLECTOR_KV

Then update wrangler.jsonc with the real database_id and KV id returned by Wrangler.

3) Initialize schema

Local:

npx wrangler d1 execute cloudflare-email-collector --local --file=schema.sql

Remote:

npx wrangler d1 execute cloudflare-email-collector --remote --file=schema.sql

4) Generate Worker types

npm run types

5) Run locally

npm run dev

6) Set production secrets

printf '%s' '<your-admin-password>' | npx wrangler secret put ADMIN_PASSWORD
openssl rand -hex 32 | npx wrangler secret put SESSION_SECRET

7) Deploy

npm run deploy

Custom domain (optional)

The repo ships with workers_dev enabled and no custom domain bound by default.

If you want your own domain, add a routes block in wrangler.jsonc after deployment, for example:

"routes": [
 {
 "pattern": "collect.example.com",
 "custom_domain": true
 }
]

Day-to-day flow

  1. super admin signs in at /admin
  2. admin creates a member account
  3. member signs in at /login
  4. member creates a short link in /app
    • collect: set a title and a quota
    • claim: paste items one per line, choose whether an email is required
  5. public users submit email (collect) or claim one item (claim) on /:shortId
  6. Durable Object increments count and closes the link when full / all claimed
  7. member/admin sees the updated state immediately, including who claimed what

Verification

npm run test
npm run check
npm run types

Security notes

  • do not commit .dev.vars
  • use Worker secrets for ADMIN_PASSWORD and SESSION_SECRET
  • member passwords are stored as hashes, not plaintext
  • the admin-created initial password is shown once in the admin UI, so rotate it on first login

FAQ

Why not store everything in KV?

Because quota counting and auto-close need a strongly consistent source of truth. KV is fine for mirrored storage, but not for authoritative real-time counters.

Why use D1 if invites already live in a Durable Object?

Because users and sessions are relational, queryable, and easier to manage in D1. Invite state and submission counting are a better fit for a single durable authority.

Can I use this as a single-user tool?

Yes. Just create one member account and ignore the rest.

How do claim links avoid double-handing-out an item?

All claim state lives in one Durable Object, so two simultaneous visitors are serialized: each gets a different item, and the last item flips the link to closed atomically. With the email gate on, re-submitting the same email returns the already-assigned item instead of consuming a new one.


License

MIT

About

Invite-only multi-user email collector built on Cloudflare Workers

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

AltStyle によって変換されたページ (->オリジナル) /