Memobee

MemoBee

A full-stack web application for managing memos and reminders with end-to-end encryption, email notifications, and speech-to-text dictation.

πŸš€ Live

πŸ“‹ Features

πŸ—οΈ Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Frontend      β”‚
β”‚  (HTML/JS/CSS)  β”‚
β”‚   Port: N/A     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚ HTTPS
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Backend API   β”‚
β”‚  Spring Boot    β”‚
β”‚   Port: 8081    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚ JDBC
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   PostgreSQL    β”‚
β”‚   Database      β”‚
β”‚   Port: 5432    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ› οΈ Tech Stack

Backend

Frontend

DevOps

πŸ“ Project Structure

Memobee/
β”œβ”€β”€ backend/
β”‚   β”œβ”€β”€ src/
β”‚   β”‚   β”œβ”€β”€ main/
β”‚   β”‚   β”‚   β”œβ”€β”€ java/com/mariusz/demo/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ DemoApplication.java
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ controller/
β”‚   β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ UserController.java
β”‚   β”‚   β”‚   β”‚   β”‚   └── NoteController.java
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ model/
β”‚   β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ User.java
β”‚   β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ Note.java
β”‚   β”‚   β”‚   β”‚   β”‚   └── PasswordResetToken.java
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ repository/
β”‚   β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ UserRepository.java
β”‚   β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ NoteRepository.java
β”‚   β”‚   β”‚   β”‚   β”‚   └── PasswordResetTokenRepository.java
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ service/
β”‚   β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ EmailService.java       (Resend HTTP API sending)
β”‚   β”‚   β”‚   β”‚   β”‚   └── ReminderScheduler.java  (5-min reminder processing)
β”‚   β”‚   β”‚   β”‚   └── security/
β”‚   β”‚   β”‚   β”‚       β”œβ”€β”€ JwtUtil.java
β”‚   β”‚   β”‚   β”‚       β”œβ”€β”€ JwtFilter.java
β”‚   β”‚   β”‚   β”‚       β”œβ”€β”€ SecurityConfig.java
β”‚   β”‚   β”‚   β”‚       └── LoginRateLimiter.java
β”‚   β”‚   β”‚   └── resources/
β”‚   β”‚   β”‚       β”œβ”€β”€ application.properties
β”‚   β”‚   β”‚       └── schema.sql
β”‚   β”‚   └── test/
β”‚   β”‚       β”œβ”€β”€ java/com/mariusz/demo/
β”‚   β”‚       β”‚   β”œβ”€β”€ SecurityIntegrationTest.java
β”‚   β”‚       β”‚   └── security/
β”‚   β”‚       β”‚       β”œβ”€β”€ JwtUtilTest.java
β”‚   β”‚       β”‚       └── LoginRateLimiterTest.java
β”‚   β”‚       └── resources/
β”‚   β”‚           └── application.properties  (test-only config, no DB required)
β”‚   β”œβ”€β”€ pom.xml
β”‚   β”œβ”€β”€ nixpacks.toml
β”‚   └── railway.json
β”œβ”€β”€ frontend/
β”‚   β”œβ”€β”€ index.html       (login / self-registration with encryption warning / forgot password)
β”‚   β”œβ”€β”€ dashboard.html   (admin panel - read-only user list with stats)
β”‚   β”œβ”€β”€ user.html        (user panel - encrypted memos / reminders / archive / change password)
β”‚   β”œβ”€β”€ reset.html       (password reset page)
β”‚   └── _headers         (Railway cache-control headers)
β”œβ”€β”€ .github/
β”‚   └── workflows/
β”‚       └── ci.yml
└── README.md

πŸ”§ Local Development Setup

Prerequisites

Backend Setup

  1. Clone the repository
    git clone <your-repo-url>
    cd "Memobee/backend"
    
  2. Configure local database

    Create a PostgreSQL database and set environment variables:

    export PGHOST=localhost
    export PGPORT=5432
    export PGDATABASE=demo_db
    export PGUSER=postgres
    export PGPASSWORD=your_password
    export PORT=8081
    
  3. Run the application
    mvn clean install
    mvn spring-boot:run
    
  4. Test the backend
    curl http://localhost:8081/api/users/health
    # Should return: "Backend is running!"
    

Frontend Setup

  1. Open the frontend
    cd ../frontend
    
  2. Serve with a local server (Python example)
    python -m http.server 8000
    
  3. Open in browser
    http://localhost:8000
    

🚒 Railway Deployment (Manual Configuration Required)

⚠️ Important: Manual Steps Required

Railway deployment requires manual configuration in the Railway dashboard. This is not fully automated CI/CD.

Backend Service Setup

  1. Create Railway Project
    • Go to Railway.app
    • Click β€œNew Project”
    • Select β€œDeploy from GitHub repo”
    • Connect your repository
  2. Add PostgreSQL Service
    • Click β€œ+ New” β†’ β€œDatabase” β†’ β€œPostgreSQL”
    • Railway will provision a PostgreSQL instance
  3. Configure Backend Service

    In the backend service settings:

    a) Set Root Directory

    • Settings β†’ Root Directory: backend

    b) Add Environment Variables (Variables tab)

    PORT = 8081
    PGHOST = $
    PGPORT = $
    PGDATABASE = $
    PGUSER = $
    PGPASSWORD = $
    JWT_SECRET = <random string, min 32 characters>
    

    c) Configure Networking

    • Settings β†’ Networking β†’ Public Networking
    • Port: 8081
    • Click β€œGenerate Domain”
  4. Deploy
    • Railway will automatically deploy on git push
    • Monitor logs in the β€œDeployments” tab

Frontend Service Setup (Optional)

If you want to deploy the frontend separately:

  1. Create New Service
    • In your Railway project, click β€œ+ New”
    • Select β€œGitHub Repo”
    • Choose the same repository
  2. Configure Frontend Service
    • Settings β†’ Root Directory: frontend
    • Generate Domain

πŸ“‘ API Endpoints

All endpoints except login and health require Authorization: Bearer <token> header. Admin-only endpoints additionally require the admin role in the token.

Public

GET  /api/users/health
POST /api/users/login              body: { email, password }
POST /api/users/register           body: { name, email, password }
POST /api/users/forgot-password    body: { email }
POST /api/users/reset-password-token  body: { token, newPassword }

Authenticated users

POST /api/users/change-password    body: { email, oldPassword, newPassword }

Admin only

GET    /api/users              list all users (includes noteCount, createdAt, lastLoginAt)
DELETE /api/users/{id}         delete user

Admin account is created automatically at startup via AdminSeeder when ADMIN_EMAIL and ADMIN_PASSWORD environment variables are set.

Notes (authenticated users β€” ownership enforced)

Users can only access their own notes. Admins can access any user’s notes.

GET    /api/notes/user/{email}   list notes for user (own or admin)
POST   /api/notes                create note (userEmail set from JWT, not request body)
PUT    /api/notes/{id}           update note (own or admin)
DELETE /api/notes/{id}           delete note (own or admin)

Note body fields by type (text field is encrypted client-side before sending):

Field Memo Reminder
type "memo" "reminder"
title βœ… plaintext βœ… plaintext β€” sent in reminder emails
text βœ… encrypted βœ… encrypted β€” NOT sent by email
frequency never/daily/weekly/monthly/quarterly/yearly (default: never) always "never"
reminderAt β€” ISO datetime UTC (e.g. "2026-03-29T12:00:00")
repeatUntilDeleted β€” true (repeat) / false (one-time)
repeatDays β€” integer β‰₯ 0 (default 0)
repeatHours β€” integer β‰₯ 0 (default 0)

πŸ§ͺ Testing

Backend Tests

Run all 38 backend tests locally:

cd backend
mvn test

Tests use a dedicated src/test/resources/application.properties that disables datasource auto-configuration β€” no PostgreSQL instance required. All repositories are mocked via @MockitoBean.

Test suites:

Suite Type Tests Covers
SecurityIntegrationTest Integration (@SpringBootTest + MockMvc) 23 Public endpoint access, unauthenticated rejection, admin-only authorization, note ownership enforcement, login/registration validation, note input validation
JwtUtilTest Unit 8 Token generation, claims extraction, expiration, tampered/invalid/wrong-key tokens
LoginRateLimiterTest Unit 7 IP-based blocking after 5 failures, reset on success, IP isolation, lockout timer

Manual API Testing with Postman

  1. Import the endpoints above into Postman
  2. Test against: https://pretty-illumination-production.up.railway.app

CI Quality Gates (GitHub Actions)

Both jobs run in parallel on every push/PR to main:

Job: validate-html

Job: test-backend

View workflow: .github/workflows/ci.yml

πŸ—„οΈ Database Schema

CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    password VARCHAR(255),
    role VARCHAR(50) DEFAULT 'user',
    encryption_salt VARCHAR(255),        -- immutable UUID for client-side encryption key derivation
    created_at TIMESTAMP,                -- UTC account creation time
    last_login_at TIMESTAMP              -- UTC last successful login
);

CREATE TABLE notes (
    id BIGSERIAL PRIMARY KEY,
    user_email VARCHAR(255) NOT NULL,
    created_at TEXT,
    type VARCHAR(20) NOT NULL,           -- memo | reminder
    title VARCHAR(255),                  -- plaintext title, used in reminder emails
    text TEXT,                           -- encrypted client-side (ENC:base64iv.base64ciphertext)
    frequency VARCHAR(20) DEFAULT 'never', -- memo: never/daily/weekly/monthly/quarterly/yearly
    attachment_name VARCHAR(255),
    attachment_type VARCHAR(50),
    attachment_data TEXT,                -- Base64 encoded
    last_sent_at TIMESTAMP,              -- set after each email is sent
    -- reminder-specific columns:
    reminder_at TIMESTAMP,               -- UTC datetime to fire the reminder
    repeat_until_deleted BOOLEAN DEFAULT FALSE,
    repeat_days INTEGER DEFAULT 0,
    repeat_hours INTEGER DEFAULT 0,
    repeat_quarters INTEGER DEFAULT 0,   -- legacy, no longer used in UI
    FOREIGN KEY (user_email) REFERENCES users(email) ON DELETE CASCADE
);

CREATE TABLE password_reset_tokens (
    id BIGSERIAL PRIMARY KEY,
    user_email VARCHAR(255) NOT NULL,
    token VARCHAR(255) NOT NULL UNIQUE,
    expires_at TIMESTAMP NOT NULL,       -- UTC, 15-minute validity
    FOREIGN KEY (user_email) REFERENCES users(email) ON DELETE CASCADE
);

πŸ” Environment Variables Reference

Backend Service (Required)

Variable Description Example
PORT Application port 8081
PGHOST PostgreSQL host $
PGPORT PostgreSQL port $
PGDATABASE Database name $
PGUSER Database user $
PGPASSWORD Database password $
JWT_SECRET HMAC-SHA signing key (min 32 chars) your-random-secret
ADMIN_EMAIL Initial admin account email (optional) admin@example.com
ADMIN_PASSWORD Initial admin account password (optional) Str0ng!Pass
RESEND_API_KEY Resend HTTP API key for sending emails re_...
MAIL_FROM Sender address (verified domain required) reminder@memobee.eu
FRONTEND_URL Frontend base URL (for password reset links) https://your-frontend.up.railway.app

Frontend Service (None required)

The frontend automatically detects whether it’s running locally or on Railway and uses the appropriate backend URL.

🚨 Common Issues & Solutions

Issue: 502 Bad Gateway on Railway

Cause: Port mismatch between app and Railway configuration

Solution:

  1. Ensure PORT=8081 is set in Railway variables
  2. Verify Railway Networking port is set to 8081
  3. Check logs to confirm Tomcat started on port 8081

Issue: Database Connection Failed

Cause: Missing or incorrect database environment variables

Solution:

  1. Verify all PG* variables are set in Railway
  2. Ensure they reference the Postgres service: $
  3. Check that backend service is linked to Postgres service

Issue: CORS Errors

Cause: Backend not allowing cross-origin requests

Solution: Already configured with @CrossOrigin(origins = "*") in UserController.java

πŸ“š Configuration Files Explained

backend/nixpacks.toml

Configures Railway’s Nixpacks build system:

backend/railway.json

Defines Railway deployment settings:

backend/application.properties

Spring Boot configuration:

backend/src/test/resources/application.properties

Test-only Spring Boot configuration:

.github/workflows/ci.yml

GitHub Actions workflow:

πŸ” Encryption

Note text is encrypted client-side before being sent to the server. The database only stores ciphertext β€” admins cannot read user content.

How it works:

  1. At registration, a random encryptionSalt (UUID) is generated and stored permanently in the database
  2. On login, the salt is loaded into sessionStorage and used to derive an AES-256-GCM key via PBKDF2 (600,000 iterations, SHA-256)
  3. Each note’s text is encrypted before save and decrypted after fetch
  4. Encrypted text format: ENC: prefix + base64(IV) + . + base64(ciphertext)
  5. The encryption key survives password changes β€” only account deletion removes it

Title vs. Text:

Limitations:

🎯 Future Improvements

πŸ‘€ Author

Mariusz Puto

πŸ“„ License

Copyright (c) 2026 Mariusz Puto. All rights reserved.

This source code is published for portfolio and demonstration purposes only. No part of this repository may be reproduced, copied, modified, distributed, or used without prior written permission from the author. See LICENSE for details.

πŸ™ Acknowledgments