A perfume formulation and accord management system for DIY perfumers and enthusiasts.
- Backend: Koa.js 3.1.1 + TypeScript + Prisma + Zod + JWT Authentication
- Frontend: Vue.js 3 + TypeScript + Pinia + Naive UI + Tailwind CSS v4
- Database: PostgreSQL
- Testing: Vitest + Supertest (backend), Vitest (frontend)
- Design System: Notion-inspired minimalist UI
- JWT-based authentication with refresh tokens
- Automatic token refresh (15-min access, 7-day refresh)
- Rate limiting on auth endpoints (brute force protection)
- Multi-user support with complete data isolation
- Secure password hashing (bcrypt)
- Persistent sessions with logout-all capability
- Token rotation for enhanced security
- Invitation-only registration system
- Manage perfume accords and essential oils
- Track pyramid position (top, middle, base notes)
- Inventory management (volume tracking in ml and drops)
- Rich tagging system with 57+ predefined tags
- Advanced filtering by position, volume, supplier, tags
- Full-text search across names, notes, and metadata
- Low stock warnings and inventory alerts
- Create and manage perfume recipes with versioning
- Recipe versions with ingredient tracking
- Ingredient volume validation against accord inventory
- Recipe notes (general, testing, observation, adjustment, reminder)
- Recipe tagging and search
- Recipe collections for organization
- Version duplication for iterating on formulas
- Collection statistics dashboard
- Pyramid position distribution analysis
- Tag and supplier usage statistics
- Volume analytics (min/max/average)
- Low inventory alerts (< 10ml threshold)
- Export collection as JSON
- Import accords from JSON backup
- Interactive REPL for administration
- User management (create, list, delete, reset passwords)
- Future: Accord/recipe import/export commands
- Future: Database operations and maintenance
- Notion-inspired clean, minimalist interface
- Fully responsive design (desktop, tablet, mobile)
- Keyboard shortcuts
- Collapsible sidebar navigation
- Skeleton loading states with shimmer animation
- Toast notifications for user feedback
- Node.js 20+
- Docker & Docker Compose (for PostgreSQL)
- npm
-
Start PostgreSQL:
docker compose up -d
-
Backend Setup:
cd backend npm install cp .env.example .env # Edit .env to set JWT_SECRET for production npx prisma generate npx prisma db push npm run dev
Backend runs at http://localhost:3000
-
Frontend Setup (in a new terminal):
cd frontend npm install npm run devFrontend runs at http://localhost:5173
-
Access the app:
- Open http://localhost:5173
- You'll need an invitation code to register
- See Creating Invitations below
Create backend/.env from the example:
PORT=3000
NODE_ENV=development
DATABASE_URL=postgres://admin:password@localhost:5435/scentora?sslmode=disable
JWT_SECRET=your-secret-key-change-in-production
JWT_ACCESS_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d
CORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000Scentora uses an invitation-only registration system.
# Connect to PostgreSQL
docker exec -it scentora-postgres psql -U admin -d scentora
# Create a first user manually (password: 'password')
INSERT INTO users (email, username, password_hash)
VALUES ('admin@example.com', 'admin', '$2a$10$your-bcrypt-hash');
# Create an invitation
INSERT INTO invitations (code, created_by, expires_at)
VALUES ('my-invite-code', (SELECT id FROM users LIMIT 1), NOW() + INTERVAL '7 days');curl -X POST "http://localhost:3000/api/invitations" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{"email": "friend@example.com", "expiresInDays": 7}'scentora/
├── backend/ # Koa.js + TypeScript API
│ ├── prisma/
│ │ └── schema.prisma # Database schema (14 models)
│ ├── src/
│ │ ├── middleware/ # Auth, error handling, rate limiting
│ │ ├── routes/ # API route handlers
│ │ ├── services/ # Business logic
│ │ ├── schemas/ # Zod validation schemas
│ │ ├── utils/ # Errors, response helpers
│ │ ├── types/ # TypeScript types
│ │ ├── app.ts # Koa app setup
│ │ └── index.ts # Entry point
│ └── tests/ # Vitest integration tests
├── frontend/ # Vue.js 3 SPA
│ ├── src/
│ │ ├── components/ # Vue components
│ │ ├── views/ # Page views
│ │ ├── stores/ # Pinia stores
│ │ ├── services/ # API client
│ │ └── types/ # TypeScript interfaces
│ └── ...
├── specs/ # API and design specifications
├── deploy/ # Deployment scripts (setup-droplet.sh)
├── .github/workflows/ # CI/CD (test.yml, deploy.yml)
├── docker-compose.yml # Dev PostgreSQL container
├── docker-compose.prod.yml # Production Docker Compose
└── README.md
POST /api/auth/register- Register (requires invitation code)POST /api/auth/login- LoginPOST /api/auth/refresh- Refresh tokensPOST /api/auth/logout- Logout (revoke refresh token)GET /api/auth/me- Get current userPOST /api/auth/logout-all- Revoke all sessions
POST /api/invitations- Create invitationGET /api/invitations- List invitationsDELETE /api/invitations/:code- Revoke invitation
POST /api/accords- Create accordGET /api/accords- List accords (with filters)GET /api/accords/:id- Get accordPUT /api/accords/:id- Update accordDELETE /api/accords/:id- Delete accordPOST /api/accords/:id/tags- Add tagDELETE /api/accords/:id/tags/:tag- Remove tag
GET /api/tags- List all predefined tagsGET /api/tags/search?q=...- Search tagsGET /api/tags/categories- List categoriesGET /api/tags/grouped- Tags grouped by categoryGET /api/tags/category/:category- Tags by category
GET /api/stats- Collection statistics
GET /api/export- Export accords as JSONPOST /api/export/import- Import accords from JSON
POST /api/recipes- Create recipeGET /api/recipes- List recipesGET /api/recipes/search?q=...- Search recipesGET /api/recipes/:id- Get recipePUT /api/recipes/:id- Update recipeDELETE /api/recipes/:id- Delete recipePOST /api/recipes/:id/versions- Create versionGET /api/recipes/:id/versions- List versionsGET /api/recipes/:id/versions/:versionId- Get versionPOST /api/recipes/:id/versions/:versionNumber/activate- Activate versionPOST /api/recipes/:id/versions/:versionId/duplicate- Duplicate versionPOST /api/recipes/:id/versions/:versionId/ingredients- Add ingredientPUT /api/recipes/:id/versions/:versionId/ingredients/:ingredientId- Update ingredientDELETE /api/recipes/:id/versions/:versionId/ingredients/:ingredientId- Remove ingredientPOST /api/recipes/:id/notes- Create noteGET /api/recipes/:id/notes- List notesPUT /api/recipes/:id/notes/:noteId- Update noteDELETE /api/recipes/:id/notes/:noteId- Delete notePOST /api/recipes/:id/tags- Add tagDELETE /api/recipes/:id/tags/:tag- Remove tagGET /api/recipes/tags/popular- Popular tags
POST /api/collections- Create collectionGET /api/collections- List collectionsGET /api/collections/:id- Get collectionPUT /api/collections/:id- Update collectionDELETE /api/collections/:id- Delete collectionPOST /api/collections/:id/recipes- Add recipe to collectionDELETE /api/collections/:id/recipes/:recipeId- Remove recipe from collection
GET /api/health- Health check
Interactive command-line interface for administration tasks.
cd backend
npm run consoleAvailable Commands (Phase 13.1):
create-user- Create new user accountlist-users- Display all users in formatted tabledelete-user <email|id>- Delete user with confirmationreset-password <email|id>- Reset user passwordshow-user <email|id>- Display detailed user information
Features:
- Auto-complete with Tab
- Command history (↑/↓ arrows)
- Colored output and progress indicators
- Interactive prompts with validation
Future commands (Phases 13.2-13.4):
- Accord/recipe import and export
- Database operations (migrations, seeding, backups)
See docs/CLI_CONSOLE.md for complete documentation.
# Start PostgreSQL first
docker compose up -d
# Run backend tests
cd backend
npm test70 integration tests across auth, accords, invitations, tags, stats, export/import, recipes, and collections.
- JWT refresh tokens with automatic rotation
- Short-lived access tokens (15 minutes)
- Rate limiting on auth endpoints (5 requests/15 min)
- Bcrypt password hashing (cost 10)
- SHA-256 hashed refresh tokens in database
- Token revocation (logout/logout-all)
- Complete user data isolation
Production deployment (https://scentora.thejoeshow.net):
- JWT_SECRET and DB_PASSWORD are generated by the setup script
- HTTPS via Let's Encrypt with auto-renewal
- Rate limiting disabled in test environment (
NODE_ENV=test) to prevent CI failures - Consider migrating rate limiting to Redis for multi-instance deployments
Scentora is deployed at https://scentora.thejoeshow.net on a DigitalOcean droplet with Docker, PostgreSQL, and automated CI/CD.
CI/CD Pipeline: GitHub Actions runs tests on PRs, then builds and deploys on push to main.
Infrastructure:
- 3 Docker containers: frontend (nginx:alpine), backend (Node.js), postgres (15-alpine)
- Host nginx reverse proxy with Let's Encrypt SSL (auto-renewal)
- Daily database backups at 2 AM (30-day retention)
- UFW firewall (ports 22, 80, 443 only)
Quick Deploy (fresh Ubuntu 24.04 droplet):
curl -sSL https://raw.githubusercontent.com/xupit3r/scentora/main/deploy/setup-droplet.sh | bash -s your-domain.com your@email.comSee Deployment Guide for complete instructions.
- Deployment Guide - Complete production deployment guide
- API Spec - Accord API specification
- Recipe API - Recipe system API specification
- Data Models - Database schema
- Tag System - Predefined tag categories
MIT