A desk booking web app for a single-floor office space. Users can view an interactive floor plan, book desks by day or full week, and manage their reservations. Admins upload the floor plan and configure desk equipment.
- Requirements
- Quick start
- Environment variables
- Available scripts
- Project structure
- Architecture
- API reference
- Authentication
- Roles and permissions
- Database
- Testing
- Seed accounts
- Deployment
| Tool | Minimum version |
|---|---|
| Node.js | 18.x |
| npm | 9.x |
No external services are required — the database is a local SQLite file.
# 1. Install dependencies
npm install
# 2. Push the database schema
npm run db:push
# 3. Seed demo users and desks
npm run db:seed
# 4. Start the development server
npm run devOpen http://localhost:3000 and sign in with one of the seed accounts.
Create a .env file at the project root (already provided for local development):
# SQLite database path (relative to project root)
DATABASE_URL="file:./dev.db"
# Secret used to sign NextAuth JWTs — change this in production
NEXTAUTH_SECRET="deskly-super-secret-key-change-in-production"
# Public URL of the app (used by NextAuth for redirects)
NEXTAUTH_URL="http://localhost:3000"Production note: Generate a strong
NEXTAUTH_SECRETwithopenssl rand -base64 32and setNEXTAUTH_URLto your public domain.
| Command | Description |
|---|---|
npm run dev |
Start the Next.js development server on port 3000 |
npm run build |
Build the app for production |
npm run start |
Start the production server (requires build first) |
npm run lint |
Run ESLint across the project |
npm run db:push |
Apply the Prisma schema to the database (creates dev.db if absent) |
npm run db:seed |
Populate the database with demo users and desks |
npm run db:studio |
Open Prisma Studio — a visual database browser at port 5555 |
deskly/
├── prisma/
│ ├── schema.prisma # Database schema (SQLite)
│ └── seed.ts # Demo data seed script
├── public/
│ └── uploads/ # Floor plan images (created at runtime, gitignored)
├── src/
│ ├── app/
│ │ ├── (auth)/
│ │ │ ├── login/ # /login — sign-in page
│ │ │ └── register/ # /register — account creation page
│ │ ├── admin/ # /admin — floor plan editor (admin only)
│ │ ├── bookings/ # /bookings — bookings list with filter
│ │ ├── api/
│ │ │ ├── auth/
│ │ │ │ ├── [...nextauth]/ # NextAuth handler (GET + POST)
│ │ │ │ └── register/ # POST — create account
│ │ │ ├── bookings/
│ │ │ │ ├── route.ts # GET (list) + POST (create booking)
│ │ │ │ └── [id]/route.ts # DELETE (cancel booking)
│ │ │ ├── desks/
│ │ │ │ ├── route.ts # GET (list) + POST (create desk, admin)
│ │ │ │ └── [id]/route.ts # PUT (update) + DELETE (admin)
│ │ │ ├── floor-plan/route.ts # GET — current floor plan record
│ │ │ └── upload/route.ts # POST — upload floor plan image (admin)
│ │ ├── globals.css
│ │ ├── layout.tsx # Root layout with SessionProvider and Header
│ │ ├── page.tsx # / — interactive floor plan (booking view)
│ │ └── providers.tsx # Client-side NextAuth SessionProvider wrapper
│ ├── components/
│ │ ├── BookingModal.tsx # Day / week booking form modal
│ │ ├── BookingsList.tsx # Filterable bookings table with cancel action
│ │ ├── DeskMarker.tsx # Coloured desk pin on the floor plan
│ │ ├── FloorPlanEditor.tsx # Admin: upload image + place / edit desks
│ │ ├── FloorPlanViewer.tsx # User: view plan + trigger booking
│ │ └── Header.tsx # Top navigation bar
│ ├── lib/
│ │ ├── auth.ts # NextAuth options (credentials provider + JWT callbacks)
│ │ ├── db.ts # Prisma client singleton
│ │ └── utils.ts # cn(), date helpers, parseEquipment()
│ ├── middleware.ts # Protects /admin and /bookings from unauthenticated access
│ └── types/
│ ├── index.ts # Shared TypeScript interfaces
│ └── next-auth.d.ts # NextAuth session/JWT type augmentation
├── .env # Local environment variables (gitignored)
├── .gitignore
├── next.config.mjs
├── postcss.config.js
├── tailwind.config.ts
└── tsconfig.json
| Layer | Technology |
|---|---|
| Framework | Next.js 14 (App Router) |
| Language | TypeScript |
| Styling | Tailwind CSS |
| Database | SQLite via Prisma ORM |
| Authentication | NextAuth v4 — credentials strategy, JWT sessions |
| Password hashing | bcryptjs (cost factor 12) |
| Validation | Zod |
| Date handling | date-fns |
| Icons | lucide-react |
NextAuth is configured with strategy: "jwt" — no database session table is needed. The JWT payload carries id and role, which are exposed on the session.user object.
Desk coordinates (x, y) are stored as percentages (0–100) of the floor plan image dimensions. This makes the layout resolution-independent — desks scale correctly with any image size and screen width.
The equipment column on the Desk table is a JSON-serialised string (e.g. '["Standing desk","Dual monitors"]'). The parseEquipment() utility in src/lib/utils.ts handles deserialisation, and all API responses return equipment as a proper string[].
The POST /api/bookings handler queries for any existing booking on the same desk where the date ranges overlap (startDate <= newEnd AND endDate >= newStart). If a conflict is found the request returns 409 Conflict.
All endpoints require a valid session cookie except where noted.
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/api/auth/register |
Public | Create a new user account |
GET/POST |
/api/auth/[...nextauth] |
Public | NextAuth sign-in / sign-out / session |
Register body
{ "email": "user@example.com", "password": "min6chars", "name": "Optional Name" }| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/desks |
Any user | List all desks |
POST |
/api/desks |
Admin | Create a desk |
PUT |
/api/desks/:id |
Admin | Update desk name / position / equipment |
DELETE |
/api/desks/:id |
Admin | Delete a desk (cascades bookings) |
Create / update desk body
{
"name": "Desk A1",
"x": 20.5,
"y": 34.1,
"equipment": ["Standing desk", "Dual monitors"]
}| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/bookings |
Any user | List all bookings |
GET |
/api/bookings?mine=true |
Any user | List only the caller's bookings |
POST |
/api/bookings |
Any user | Create a booking |
DELETE |
/api/bookings/:id |
Owner or Admin | Cancel a booking |
Create booking body
{
"deskId": "desk-1",
"date": "2026-06-16",
"type": "DAY"
}type is "DAY" or "WEEK". For WEEK, date can be any day within the target week — the API calculates Monday–Sunday automatically.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/floor-plan |
Any user | Get the current floor plan record |
POST |
/api/upload |
Admin | Upload a new floor plan image (multipart/form-data, field: file) |
The app uses email + password authentication via NextAuth's credentials provider.
- Passwords are hashed with bcrypt (cost factor 12) before storage.
- Sessions are stateless JWTs stored in an HTTP-only cookie.
- The
rolefield (USERorADMIN) is embedded in the token and validated server-side on every protected request. - The NextAuth middleware (
src/middleware.ts) guards/admin/*and/bookings/*— unauthenticated requests are redirected to/login.
| Action | USER | ADMIN |
|---|---|---|
| View floor plan | ✅ | ✅ |
| Book a desk | ✅ | ✅ |
| Cancel own booking | ✅ | ✅ |
| Cancel any booking | ❌ | ✅ |
| Upload floor plan | ❌ | ✅ |
| Create / edit / delete desks | ❌ | ✅ |
Access /admin |
❌ | ✅ |
To promote a user to admin, update their role column directly via Prisma Studio (npm run db:studio) or a SQL query:
UPDATE User SET role = 'ADMIN' WHERE email = 'user@example.com';The Prisma schema defines four models:
User — id, email (unique), name, password (bcrypt), role, timestamps
FloorPlan — id, imageUrl, timestamps (only one record is used; updated in-place)
Desk — id, name, x, y, equipment (JSON string), timestamps
Booking — id, userId → User, deskId → Desk, startDate, endDate, timestamps
# Apply schema changes to the database without generating a migration file
npm run db:push
# Open the visual database browser
npm run db:studio
# Re-seed (safe — uses upsert, will not duplicate records)
npm run db:seed
# Generate the Prisma client after schema changes
npx prisma generateThe project currently has no automated test suite. Below are the manual test scenarios that cover all key behaviours.
| Password | Role | |
|---|---|---|
admin@deskly.com |
admin123 |
Admin |
alice@deskly.com |
user123 |
User |
bob@deskly.com |
user123 |
User |
| # | Steps | Expected result |
|---|---|---|
| 1.1 | Open /register, fill in name + email + password (≥ 6 chars), submit |
Account created, redirected to / |
| 1.2 | Open /register with an already-used email |
Error: "Email already in use" |
| 1.3 | Open /login, enter correct credentials |
Signed in, redirected to / |
| 1.4 | Open /login, enter wrong password |
Error: "Invalid email or password" |
| 1.5 | Navigate to /bookings while signed out |
Redirected to /login |
| 1.6 | Navigate to /admin while signed in as a regular user |
Redirected to / |
| # | Steps | Expected result |
|---|---|---|
| 2.1 | Sign in as admin, go to /admin |
Upload button visible, empty plan area |
| 2.2 | Upload a PNG/JPG/WebP/SVG image | Image rendered as the floor plan |
| 2.3 | Click anywhere on the image | Dashed circle appears at click point, "New desk" form opens |
| 2.4 | Enter desk name, check equipment, click "Create desk" | Coloured marker appears on the plan |
| 2.5 | Click an existing marker, edit name or equipment, save | Marker label and sidebar list update |
| 2.6 | Hover a marker, click the red × | Confirmation dialog → desk and all its bookings are deleted |
| 2.7 | Upload a non-image file | Error: "Invalid file type" |
| # | Steps | Expected result |
|---|---|---|
| 3.1 | Sign in as Alice, go to / |
Floor plan shows green (available) markers |
| 3.2 | Click a green marker | Booking modal opens with desk name and equipment |
| 3.3 | Select "Single day", pick a date, confirm | Modal closes; marker turns blue (mine) |
| 3.4 | Click the same desk again on the same date | Modal shows the desk as unavailable / booking fails with conflict error |
| 3.5 | Sign in as Bob, go to / |
The desk Alice booked shows as red (taken) |
| 3.6 | Select "Full week", pick any day in the week, confirm | Desk is booked Mon–Sun; adjacent weeks remain green |
| 3.7 | Switch the date filter to a day outside a booking period | Previously booked desk appears green again |
| # | Steps | Expected result |
|---|---|---|
| 4.1 | Go to /bookings |
All bookings from all users shown under "Upcoming" |
| 4.2 | Click "My bookings" filter | Only the signed-in user's bookings remain |
| 4.3 | Own upcoming booking has a "Cancel" button | Clicking it removes the booking from the list |
| 4.4 | Another user's booking has no cancel button | No cancel button visible |
| 4.5 | Sign in as admin, go to /bookings |
Admin sees all bookings; "Cancel" button is visible only on their own entries |
# Attempt to create a desk without auth — expect 403
curl -X POST http://localhost:3000/api/desks \
-H "Content-Type: application/json" \
-d '{"name":"X","x":50,"y":50,"equipment":[]}'
# Attempt to delete another user's booking — expect 403
curl -X DELETE http://localhost:3000/api/bookings/<booking-id>To return the database to a clean seeded state:
rm prisma/dev.db
npm run db:push
npm run db:seednpm run build
npm run start- Set a strong
NEXTAUTH_SECRET(openssl rand -base64 32) - Set
NEXTAUTH_URLto the public URL of the app - Set
DATABASE_URL— for a hosted environment consider replacing SQLite with PostgreSQL by changing the Prisma provider and runningnpx prisma migrate deploy - Ensure the
public/uploads/directory is writable and persisted across deployments (use a CDN or object storage for production) - Create the initial admin account manually (register normally, then set
role = 'ADMIN'in the database)