A full-stack web app for writing notes to your future self and unlocking them at a chosen date.
Tether Note lets users create time-locked notes, receive reminders before unlock time, and revisit messages when they become available. The app is designed as a portfolio project that demonstrates product thinking, full-stack architecture, and polished UI execution.
- React 19 + Vite
- React Router
- CSS modules/files for custom UI styling
use-soundfor ambient/audio interactions
- Node.js + Express
- MongoDB + Mongoose
- JWT-based authentication
- Nodemailer + cron-based reminder service
- Docker (containerised for deployment)
- Secure sign up/sign in flow
- Create future notes with unlock dates
- Read available notes in a dashboard experience
- View locked/unlocked states for each note
- Email reminder pipeline before unlock time
- Profile management endpoints
.
├── backend
│ ├── controllers
│ ├── database
│ ├── middleware
│ ├── models
│ ├── routes
│ ├── services
│ ├── app.js
│ └── Dockerfile
└── frontend
├── public
│ └── assets
│ ├── images
│ │ └── envelopes
│ ├── fonts
│ └── audio
└── src
├── api
├── components
│ ├── auth
│ ├── dashboard
│ └── ui
├── constants
├── hooks
├── pages
├── styles
├── utils
└── App.jsx
npm install
cd backend && npm install
cd ../frontend && npm installCreate backend/.env with:
PORT=5000
DB_URI=your_mongodb_connection_string
JWT_SECRET=your_jwt_secret
JWT_EXPIRES_IN=7d
FRONTEND_URL=http://localhost:5173
FRONTEND_URLS=http://localhost:5173
EMAIL_USER=your_gmail_address
EMAIL_PASSWORD=your_gmail_app_password
JOB_SECRET=a_long_random_secret_for_manual_triggers
CRON_SECRET=a_long_random_secret_for_vercel_cronIn one terminal:
cd backend
npm run devIn another terminal:
cd frontend
npm run devMake sure Docker Desktop is running, then from inside the backend/ folder:
docker build -t tether-note-backend:latest .Test it locally:
docker run --name tether-backend -d -p 5000:5000 \
-e PORT=5000 \
-e DB_URI=your_mongo_uri \
-e JWT_SECRET=your_secret \
-e JWT_EXPIRES_IN=7d \
-e EMAIL_USER=your_email \
-e EMAIL_PASSWORD=your_password \
-e FRONTEND_URL=http://localhost:5173 \
tether-note-backendVisit http://localhost:5000/health to confirm it's running.
# Log in
docker login -u your_dockerhub_username
# Tag the image
docker tag tether-note-backend:latest your_dockerhub_username/tether-note-backend:latest
# Push
docker push your_dockerhub_username/tether-note-backend:latest- Go to render.com and log in (no credit card needed for free tier).
- Click New → Web Service.
- Choose "Deploy an existing image from a registry".
- Enter your image URL:
docker.io/your_dockerhub_username/tether-note-backend:latest - Give it a name, pick a region, select the Free instance type.
- Under Environment Variables, add:
NODE_ENV=productionPORT=5000DB_URI=...JWT_SECRET=...JWT_EXPIRES_IN=7dEMAIL_USER=...EMAIL_PASSWORD=...FRONTEND_URL=https://<your-vercel-domain>FRONTEND_URLS=https://<your-vercel-domain>
- Click Deploy. Render will give you a public URL like
https://tether-note-backend.onrender.com.
Rebuild and push a new image — Render will automatically redeploy:
docker build -t tether-note-backend:latest .
docker tag tether-note-backend:latest your_dockerhub_username/tether-note-backend:latest
docker push your_dockerhub_username/tether-note-backend:latestNote: Render's free tier spins down after 15 minutes of inactivity. The first request after idle may be slow while the instance wakes up.
- Import this repo in Vercel and set Root Directory to
frontend. - Build settings can stay default for Vite (
npm run build, outputdist). - In Vercel Project Environment Variables, set:
VITE_API_URL=https://<your-render-backend-url>
- Redeploy after env changes.
If your backend can sleep (free tier) or runs on serverless, in-process node-cron may not run continuously.
For Vercel deployments, this repo now includes a Vercel Cron schedule in backend/vercel.json that calls:
GET /jobs/process-emailsonce per day
Set CRON_SECRET in Vercel project environment variables. Vercel Cron sends this as a bearer token automatically.
For manual/external triggers (cron-job.org, Render Cron, GitHub Actions, etc.), call:
POST /jobs/process-emails- Header:
x-job-secret: <JOB_SECRET>(orAuthorization: Bearer <JOB_SECRET>)
This guarantees reminder/unlock emails are processed even when the app is not always-on.
Backend CORS allows origins listed in FRONTEND_URLS (comma-separated) or FRONTEND_URL. If you use a custom Vercel domain, add it to FRONTEND_URLS.
POST /auth/sign-upPOST /auth/sign-inGET /user(auth required)PUT /user(auth required)GET /notes(auth required)POST /notes(auth required)GET /notes/:id(auth required)DELETE /notes/:id(auth required)GET /health
- Product & development: Nourin Awad
- Built with: React, Vite, Node.js, Express, MongoDB, Mongoose, Docker
- Audio helper package:
use-sound
This project is currently shared as a portfolio project. Add a formal license if you plan to open-source it publicly.