Skip to content

davidballester/bragreminder.com

Repository files navigation

Brag Reminder

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

Stack

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)
Email Resend (reminder emails, enabled by default with opt-out)

Project structure

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.

Styling

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.

Prerequisites

  • 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)

Setup

1. Install dependencies

npm install

2. Configure environment variables

Frontend (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.

3. Deploy Firestore security rules

firebase deploy --only firestore:rules

4. (Optional) Enable Firestore TTL for entry tokens

The 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 writes expiresAt as a timestamp).
  • Deletion is asynchronous/eventual, so runtime token-expiry checks remain required.

5. Generate VAPID keys (optional)

npx @pushforge/builder vapid

Use 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.

Development

# Frontend
npm run dev:web

# Worker
npm run dev:worker

Testing

# All workspaces
npm test

# Individual
npm test -w @brag-doc/web
npm test -w @brag-doc/worker

Type checking & linting

npm run typecheck
npm run lint
npm run format          # check formatting
npm run format:write    # fix formatting

Deployment

Frontend (Cloudflare Pages)

Connect 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.

Worker (Cloudflare Workers)

cd apps/worker
npx wrangler deploy

The 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 expired entryTokens records 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.

Triggering a notification on demand

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.

How it works

  1. 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.
  2. Optionally enable push notifications — once enabled in Settings, the browser subscription is stored in Firestore via the Worker API.
  3. 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.
  4. Automatic entry-link cleanup — a daily cron removes expired one-time entry-link tokens from entryTokens.
  5. 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.
  6. Export — download all entries as a single bragging-YYYY-MM-DD.md file.
  7. 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.

Language switching

  • 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 (/en or /es).
  • The footer also includes a localized link to the legal notice page.

License

Private.

About

Don't forget to write your professional wins every other day with this reminder tool

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors