Capture your professional wins before they disappear. Set a reminder cadence, jot down entries from reminders, and export everything to a clean Markdown file when review season comes around.
Live site: https://bragreminder.com
| Layer | Technology |
|---|---|
| Frontend | Next.js + Tailwind CSS (static export) on Cloudflare Pages |
| Backend | Cloudflare Workers |
| Database | Firebase Firestore |
| Auth | Firebase Authentication |
| Push | Web Push API (VAPID, optional, disabled by default) |
| Resend (reminder emails, enabled by default with opt-out) |
apps/
web/ Next.js frontend with Tailwind CSS (static export)
worker/ Cloudflare Worker (cron, push, email, entry-link APIs)
packages/
config/ Shared configuration (reserved)
This is an npm workspaces monorepo. The frontend has no server runtime in production — all backend logic lives in the Worker.
The frontend uses Tailwind CSS utilities.
- Tailwind entrypoint:
apps/web/app/globals.css - Tailwind config:
apps/web/tailwind.config.js - PostCSS config:
apps/web/postcss.config.js - Styling guide:
docs/styling-guide.md
When adding or changing UI styles, prefer Tailwind utility classes directly in page and component markup.
- Node.js ≥ 20
- A Firebase project with Firestore and Email/Password auth enabled
- A Resend account (for reminder emails)
- VAPID key pair for Web Push (optional, only required when enabling push)
- Cloudflare account (Workers + Pages)
npm installFrontend (apps/web/.env.local):
NEXT_PUBLIC_FIREBASE_API_KEY=...
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=...
NEXT_PUBLIC_FIREBASE_PROJECT_ID=...
NEXT_PUBLIC_FIREBASE_APP_ID=...
NEXT_PUBLIC_WORKER_BASE_URL=https://brag-doc-worker.<your-subdomain>.workers.dev
NEXT_PUBLIC_VAPID_PUBLIC_KEY=... # optional (required only if push notifications are enabled)
In Firebase Authentication, add every web origin you use for the app to Authorized domains. This includes local development origins and your deployed app domain if you want password-reset emails to return users to the dedicated reset page in the app after they finish resetting their password.
Worker (set via wrangler secret put <NAME> or in the Cloudflare dashboard):
FIREBASE_PROJECT_ID
FIREBASE_PRIVATE_KEY
FIREBASE_CLIENT_EMAIL
VAPID_PUBLIC_KEY # optional (required only if push notifications are enabled)
VAPID_PRIVATE_KEY # optional (required only if push notifications are enabled)
RESEND_API_KEY
RESEND_FROM_EMAIL
ENTRY_LINK_SECRET # random secret for signing one-time entry tokens
SCHEDULER_SECRET # random secret protecting the /api/notify/run endpoint
NOTIFY_SECRET # random secret protecting the /api/notify/trigger endpoint
WEB_APP_BASE_URL # e.g. https://brag.yourdomain.com
WORKER_BASE_URL # e.g. https://brag-doc-worker.<subdomain>.workers.dev (used for email unsubscribe links)
Firebase ID token verification in the Worker uses jose with Google's JWKS endpoint, so key rotation is handled automatically and no additional token-verification env vars are required.
firebase deploy --only firestore:rulesThe Worker now runs a daily cleanup job that deletes expired documents from entryTokens, so Firestore TTL is optional.
If you have billing enabled and want defense in depth, you can still configure a Firestore TTL policy for collection entryTokens on field expiresAt.
- Field type must be Firestore
Timestamp(the worker writesexpiresAtas a timestamp). - Deletion is asynchronous/eventual, so runtime token-expiry checks remain required.
npx @pushforge/builder vapidUse the generated public key as both VAPID_PUBLIC_KEY in the web app and VAPID_PUBLIC_KEY in the worker.
Set VAPID_PRIVATE_KEY in the worker to the generated raw base64url private key string exactly as emitted by the generator. Do not convert it to PEM or JWK.
# Frontend
npm run dev:web
# Worker
npm run dev:worker# All workspaces
npm test
# Individual
npm test -w @brag-doc/web
npm test -w @brag-doc/workernpm run typecheck
npm run lint
npm run format # check formatting
npm run format:write # fix formattingConnect the repo to Cloudflare Pages with:
- Build command:
npm run build -w @brag-doc/web - Build output directory:
apps/web/out - Root directory:
/
Set the NEXT_PUBLIC_* environment variables in the Pages dashboard.
cd apps/worker
npx wrangler deployThe Worker has two cron triggers:
0 * * * *(hourly): evaluates each user's notification preferences and sends push or email reminders when due.20 2 * * *(daily): deletes expiredentryTokensrecords so one-time entry links do not accumulate.
Push notifications are disabled by default and can be enabled from Settings. Email reminders are enabled by default and can be turned off from Settings or via the unsubscribe link included in reminder emails.
POST /api/notify/trigger sends a notification immediately to any user, bypassing scheduling checks. Authenticate with NOTIFY_SECRET via header or bearer token:
# By userId
curl -X POST https://<worker-url>/api/notify/trigger \
-H "x-notify-secret: <NOTIFY_SECRET>" \
-H "Content-Type: application/json" \
-d '{"userId": "<firebase-uid>"}'
# By email
curl -X POST https://<worker-url>/api/notify/trigger \
-H "Authorization: Bearer <NOTIFY_SECRET>" \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com"}'
# With a custom prompt
curl -X POST https://<worker-url>/api/notify/trigger \
-H "x-notify-secret: <NOTIFY_SECRET>" \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "prompt": "What shipped this sprint?"}'Response:
{ "status": "sent", "userId": "...", "pushSent": true, "emailSent": false }status is "skipped" when the user has no active push subscription and email reminders are disabled or unavailable.
- Sign up with email and configure reminder settings (frequency, time window, timezone). Push notifications start disabled by default, and email reminders start enabled by default.
- Optionally enable push notifications — once enabled in Settings, the browser subscription is stored in Firestore via the Worker API.
- Receive reminders — an hourly cron checks if it's time to nudge you, respecting your timezone and frequency. Notifications link to the entry form with a signed token.
- Automatic entry-link cleanup — a daily cron removes expired one-time entry-link tokens from
entryTokens. - Write entries — minimal friction: optional title + free text content. For notification links, the app attempts to validate and redeem the token via the Worker when available, but authenticated users can still write and save a normal entry if the token has already been consumed or is otherwise unusable.
- Export — download all entries as a single
bragging-YYYY-MM-DD.mdfile. - Delete account — from Settings, users can permanently delete their account and stored brag data. If Firebase requires a recent login, the app asks them to authenticate again, shows a dedicated deletion-in-progress screen, and then confirms success with a link back to the home page.
- The app includes a footer language switcher on locale pages with a two-button EN/ES control.
- Selecting a language always routes to that locale home page (
/enor/es). - The footer also includes a localized link to the legal notice page.
Private.