A step up on my previous kanban board project with user authentication, persistent database and type safety. Built with TypeScript, Express and SQLite.
Railway Link: chamomile-production.up.railway.app/
- π Secure Authentication - JWT-based auth with bcrypt password hashing
- π€ Anti-Bot Protection - Honeypot fields and timing analysis
- πΎ Persistent Storage - SQLite database with automatic backups
- π Type-Safe - Full TypeScript backend with compile-time safety
- π± Responsive - Works on desktop, tablet, and mobile, with smooth drag-and-drop
Backend:
- TypeScript + Express
- SQLite (better-sqlite3)
- JWT authentication
- bcrypt password hashing
- express-rate-limit for DDoS protection
Frontend:
- Vanilla JavaScript (no frameworks)
- HTML5 drag-and-drop API
- CSS3 animations
- LocalStorage for token persistence
- Node.js 18+
- npm or yarn
- Clone the repository
git clone https://github.com/TDJR007/Chamomile.git
cd chamomile- Install dependencies
npm install- Configure environment variables
cp .env.sample .envEdit .env and set your values:
PORT=3000
JWT_SECRET=your_super_secret_jwt_key_change_this_in_production
DB_FILE=./data/chamomile.db
NODE_ENV=developmentJWT_SECRET to a strong random string in production!
- Initialize the database
npm run db:init- Start the development server
npm run dev- Open your browser
http://localhost:3000
npm run buildThis compiles TypeScript to JavaScript in the dist/ folder.
NODE_ENV=production npm startMake sure to set these in your production environment:
PORT- Server port (default: 3000)JWT_SECRET- MUST be a strong random stringDB_FILE- Path to SQLite database fileNODE_ENV- Set toproduction
chamomile/
βββ public/ # Frontend files
β βββ index.html # Main kanban board
β βββ auth.html # Login/signup page
β βββ auth.js # Auth logic
β βββ api.js # API wrapper
β βββ drag.js # Drag-and-drop
β βββ todo.js # Task creation
β βββ utils.js # Helper functions
β βββ storage.js # Data loading
β βββ star-background.js
β βββ styles.css
β
βββ src/ # TypeScript backend source
β βββ types/
β β βββ index.ts # TypeScript type definitions (User, Task, etc.)
β β
β βββ middleware/
β β βββ authMiddleware.ts # JWT token verification middleware
β β βββ tightSignupGuard.ts # Anti-bot protection (honeypot + timing)
β β βββ errorHandler.ts # Global error handling middleware
β β
β βββ routes/
β β βββ authRoutes.ts # Auth endpoints (register, login)
β β βββ todoRoutes.ts # Task CRUD endpoints (get, create, update, delete)
β β βββ index.ts # Route aggregator (combines all routes)
β β
β βββ utils/
β β βββ jwt.ts # JWT token generation and verification
β β βββ password.ts # Password hashing and comparison (bcrypt)
β β βββ rateLimiter.ts # Rate limiting configurations
β β βββ validation.ts # Input validation and sanitization
β β
β βββ db/
β β βββ database.ts # SQLite database connection and operations
β β βββ schemaInit.ts # Database schema initialization script
β β
β βββ app.ts # Express app configuration and middleware setup
β βββ server.ts # Server entry point (starts Express server)
β
βββ data/
β βββ chamomile.db # SQLite database file (created on first run)
β
βββ dist/ # Compiled JavaScript output (after npm run build)
β
βββ node_modules/ # Dependencies (not committed to git)
β
βββ .env # Environment variables (NOT in git)
βββ .env.sample # Environment variables template
βββ .gitignore # Files to exclude from git
βββ chamomile.rest # API testing file (REST Client for VS Code)
βββ nixpacks.toml # Railway build configuration
βββ package.json # Node.js dependencies and scripts
βββ package-lock.json # Locked dependency versions
βββ railway.json # Railway deployment configuration
βββ README.md # Project documentation
βββ tsconfig.json # TypeScript compiler configuration
- JWT tokens with 7-day expiration
- Bcrypt password hashing (10 rounds)
- Passwords must be 8+ characters
- Same-origin policy: Our app uses
origin: truewhich reflects the request origin; this works because frontend and backend share the same domain. - This is more secure than
origin: '*'(allows everything) and simpler than whitelisting specific domains.
- Honeypot field - Hidden form field that bots auto-fill
- Timing analysis - Detects forms filled too quickly
- Rate limiting:
- Signup: 3 attempts per 24 hours
- Login: 10 attempts per 15 minutes
- API: 100 requests per 15 minutes
- SQL injection protection via prepared statements
- Foreign key constraints with CASCADE deletion
- User data isolation (users can only access their own tasks)
Use the included chamomile.rest file with VS Code's REST Client extension:
- Install REST Client extension
- Open
chamomile.rest - Update the
@tokenvariable after logging in - Click "Send Request" above any endpoint
Or use curl:
# Register
curl -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"password123","timestamp":'$(date +%s)000'}'
# Login
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"password123"}'
# Get tasks (replace YOUR_TOKEN)
curl http://localhost:3000/api/todos \
-H "Authorization: Bearer YOUR_TOKEN"POST /api/auth/register- Create new userPOST /api/auth/login- Login and get JWT token
GET /api/todos- Get all tasksPOST /api/todos- Create new taskPUT /api/todos/:id- Update taskDELETE /api/todos/:id- Delete task
Server won't start:
- Check that port 3000 isn't already in use
- Verify
.envfile exists and has valid values - Run
npm run db:initto ensure database is initialized
"Invalid token" errors:
- Token may have expired (7-day limit)
- Log out and log back in to get a new token
- Check that
JWT_SECRETis set in.env
Tasks not persisting:
- Check
data/folder exists and is writable - Verify database file was created:
ls data/chamomile.db - Check server logs for database errors
# Initialize git (if not done)
git init
git add .
git commit -m "Initial commit - Chamomile kanban board"
# Create GitHub repo, then:
git remote add origin https://github.com/yourusername/chamomile.git
git branch -M main
git push -u origin mainCreate this in your project root for better control:
{
"$schema": "https://railway.app/railway.schema.json",
"build": {
"builder": "NIXPACKS"
},
"deploy": {
"startCommand": "npm start",
"restartPolicyType": "ON_FAILURE",
"restartPolicyMaxRetries": 10
}
}Railway auto-detects Node.js apps, but let's be explicit.
Create nixpacks.toml in project root:
[phases.setup]
nixPkgs = ['nodejs_20']
[phases.install]
cmds = ['npm ci']
[phases.build]
cmds = ['npm run build']
[start]
cmd = 'npm start'This tells Railway exactly how to build your app (uses Node 20, installs deps, builds TypeScript, runs production server).
- Go to: https://railway.app
- Click "Login" β Sign up with GitHub (easiest way)
- Authorize Railway to access your repos
You'll get $5 free credit immediately (no CC needed yet).
- Click "New Project"
- Select "Deploy from GitHub repo"
- Choose your chamomile repository
- Railway will auto-detect it's a Node.js app and start deploying
Once the initial deploy starts:
- Click on your service (should say "chamomile" or similar)
- Go to "Variables" tab
- Add these variables:
NODE_ENV=production
PORT=3000
DB_FILE=/app/data/chamomile.db
JWT_SECRET=<click "Generate" button or paste your own>
For JWT_SECRET: Railway has a "Generate" button that creates a secure random string. Use that!
This is CRITICAL - without this, your database resets on every deploy!
- Under your Project, go to "Architecture" tab
- Right click on your service and click "Attach Volumes"
- Under "Create Volume"
- Set:
- Mount Path:
/app/data - Size: 1 GB (more than enough)
- Mount Path:
- Click "Add"
Railway will redeploy automatically after adding the volume.
Update src/db/database.ts to create the directory if it doesn't exist:
import Database from 'better-sqlite3';
import path from 'path';
import fs from 'fs';
import dotenv from 'dotenv';
import { User, Task, TaskStatus } from '../types';
// Load environment variables
dotenv.config();
const dbPath = process.env.DB_FILE || './data/chamomile.db';
// Ensure data directory exists (important for Railway/production)
const dbDir = path.dirname(dbPath);
if (!fs.existsSync(dbDir)) {
fs.mkdirSync(dbDir, { recursive: true });
console.log(`π Created data directory: ${dbDir}`);
}
const db = new Database(dbPath);
// Enable foreign keys (CRITICAL for CASCADE deletion)
db.pragma('foreign_keys = ON');
console.log(`π¦ Database connected: ${path.resolve(dbPath)}`);
// ... rest stays the sameCommit and push this change:
git add .
git commit -m "Ensure data directory exists for production"
git pushRailway will auto-deploy the update.
- In Railway dashboard, click on your service
- Go to "Settings" β "Networking"
- Click "Generate Domain"
You'll get something like:
https://chamomile-production-abc123.up.railway.app
Copy that URL and visit it! π
- Visit your Railway URL
- Sign up for an account
- Create some tasks
- Drag them around
- Refresh the page - data should persist!
- Log out and log back in - should still work!
# Install Railway CLI (optional but useful)
npm i -g @railway/cli
# Login
railway login
# Link to your project
railway link
# View logs
railway logsOr just view logs in the Railway dashboard: Service β Deployments β Click latest deploy β View Logs
In Railway dashboard:
- Go to Deployments
- Click "Redeploy" on latest deployment
Or push to GitHub (auto-deploys).
Railway dashboard β Metrics tab
You'll see:
- CPU usage
- Memory usage
- Network bandwidth
- Estimated cost (should be ~$3-4/month)
Check your usage:
- Railway dashboard β Account Settings β Usage
- You'll see current month's usage
- Breakdown by project/service
Pro tip: Set up a notification when you hit $4 (Settings β Notifications).
Railway will:
- Email you when you hit 80% of credit
- Pause services when credit hits $0
- You can add a card to continue (only charges what you use)
But realistically, $5 covers this app easily. You'd need to get significant traffic to burn through it.
MIT License - feel free to use this for personal or commercial projects!
Built with Claude, TypeScript, and way too much coffee β

