@@ -24,17 +24,17 @@
|
||||
|
||||
**Rationale:** Provides security (tokens can be revoked) and aligns with OAuth2 best practices for refresh token management.
|
||||
|
||||
## 5. SQLite for Development
|
||||
## 5. PostgreSQL Everywhere
|
||||
|
||||
**Decision:** SQLite is the default development database.
|
||||
**Decision:** PostgreSQL is used in development, CI and production.
|
||||
|
||||
**Rationale:** Zero configuration, fast for local development, and Prisma abstracts most differences. PostgreSQL migration path is documented in production.md.
|
||||
**Rationale:** One database means one set of migrations; the schema uses JSON columns and serializable transactions that SQLite doesn't support the same way.
|
||||
|
||||
## 6. No WebSockets
|
||||
## 6. Invalidation-Only Live Updates
|
||||
|
||||
**Decision:** Dashboard uses polling (TanStack Query) instead of WebSockets.
|
||||
**Decision:** The API pushes topic names (`tabs`, `commands`, ...) over Socket.IO; the dashboard refetches that data over the normal REST API. It falls back to polling while the socket is down.
|
||||
|
||||
**Rationale:** Simpler implementation, adequate for the expected update frequency (5-30 second intervals), and avoids WebSocket server infrastructure.
|
||||
**Rationale:** Events never carry data, so a socket can't see anything its owner couldn't fetch, and the REST API stays the single source of truth.
|
||||
|
||||
## 7. In-Memory Rate Limiting
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ KoolTap is a modular monolith designed for remote tab control in venues.
|
||||
- REST API with versioned endpoints (`/api/v1`)
|
||||
- JWT authentication for dashboard users
|
||||
- Bearer token authentication for devices
|
||||
- SQLite database via Prisma ORM
|
||||
- PostgreSQL database via Prisma ORM
|
||||
- Scheduled command expiry job
|
||||
- In-memory rate limiting
|
||||
|
||||
@@ -19,7 +19,8 @@ KoolTap is a modular monolith designed for remote tab control in venues.
|
||||
- TanStack Query for API data fetching
|
||||
- shadcn/ui component library
|
||||
- Tailwind CSS styling
|
||||
- Secure HttpOnly cookie-based auth
|
||||
- Tokens kept in cookies (see security.md)
|
||||
- Socket.IO connection for live updates, polling as fallback
|
||||
|
||||
### Shared Package
|
||||
- TypeScript types and constants
|
||||
|
||||
@@ -54,9 +54,8 @@ pnpm db:reset # Reset database and re-seed
|
||||
## Environment Variables
|
||||
|
||||
### API (`apps/api/.env`)
|
||||
- `DATABASE_URL` - SQLite path (default: `file:./dev.db`)
|
||||
- `DATABASE_URL` - PostgreSQL connection URL (`docker compose up postgres` starts one)
|
||||
- `JWT_SECRET` - JWT signing secret
|
||||
- `JWT_REFRESH_SECRET` - Refresh token secret
|
||||
- `PORT` - API port (default: 3001)
|
||||
- `CORS_ORIGIN` - Allowed CORS origin (default: http://localhost:3000)
|
||||
|
||||
|
||||
+8
-33
@@ -12,9 +12,8 @@ Create `.env` in the project root:
|
||||
# Database
|
||||
DB_PASSWORD=<generate-a-strong-password>
|
||||
|
||||
# JWT secrets — generate with: openssl rand -hex 64
|
||||
# JWT secret — generate with: openssl rand -hex 64
|
||||
JWT_SECRET=<64-char-hex-string>
|
||||
JWT_REFRESH_SECRET=<64-char-hex-string>
|
||||
|
||||
# URLs — set your actual domain/port
|
||||
DASHBOARD_URL=https://dashboard.yourdomain.com
|
||||
@@ -29,16 +28,16 @@ API_PORT=3001
|
||||
docker compose -f docker-compose.prod.yml up --build -d
|
||||
```
|
||||
|
||||
### 3. Seed the database (first time only)
|
||||
### 3. Create the venue and admin (first time only)
|
||||
|
||||
Migrations run automatically when the API container starts.
|
||||
|
||||
```bash
|
||||
# Generate Prisma client and run migrations
|
||||
docker compose -f docker-compose.prod.yml exec api npx prisma migrate deploy
|
||||
|
||||
# Seed demo data (or create your own venue/users)
|
||||
docker compose -f docker-compose.prod.yml exec api npx ts-node prisma/seed.ts
|
||||
docker compose -f docker-compose.prod.yml exec api node prisma/setup.js "My Venue" admin@myvenue.com '<strong password>'
|
||||
```
|
||||
|
||||
`setup.js` does nothing when a venue already exists. `--force` wipes ALL data and starts over.
|
||||
|
||||
### 4. Set up a reverse proxy (nginx/Caddy)
|
||||
|
||||
Example Caddyfile:
|
||||
@@ -70,22 +69,11 @@ GRANT ALL PRIVILEGES ON DATABASE kooltap TO kooltap;
|
||||
```env
|
||||
DATABASE_URL="postgresql://kooltap:your-password@localhost:5432/kooltap"
|
||||
JWT_SECRET="<random-64-char-string>"
|
||||
JWT_REFRESH_SECRET="<random-64-char-string>"
|
||||
PORT=3001
|
||||
CORS_ORIGIN="https://dashboard.yourdomain.com"
|
||||
```
|
||||
|
||||
### 3. Update Prisma schema for PostgreSQL
|
||||
|
||||
In `apps/api/prisma/schema.prisma`, change:
|
||||
```prisma
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Build and run
|
||||
### 3. Build and run
|
||||
|
||||
```bash
|
||||
cd apps/api
|
||||
@@ -98,19 +86,6 @@ node dist/main
|
||||
|
||||
---
|
||||
|
||||
## Migration Steps: SQLite → PostgreSQL
|
||||
|
||||
1. Update `schema.prisma` to use `postgresql` provider
|
||||
2. Update `DATABASE_URL` environment variable
|
||||
3. Run `prisma migrate dev` to create initial PostgreSQL migration
|
||||
4. Export SQLite data and import to PostgreSQL (using `prisma db pull` + manual script, or insert via seed)
|
||||
5. Run `prisma migrate deploy` on production
|
||||
|
||||
Key differences to handle:
|
||||
- SQLite uses `Int` for booleans → use `Boolean` in PostgreSQL
|
||||
- SQLite uses `String` for JSON → use `Json` type in PostgreSQL
|
||||
- UUID generation is handled by Prisma on both
|
||||
|
||||
## Security Checklist
|
||||
|
||||
- [ ] JWT secrets are 64+ random hex characters
|
||||
|
||||
+8
-7
@@ -11,15 +11,16 @@
|
||||
|
||||
### Client Authentication
|
||||
- Passwords hashed with bcrypt (10 rounds)
|
||||
- JWT access tokens (24h expiration)
|
||||
- Refresh tokens with rotation and revocation
|
||||
- Tokens stored in HttpOnly cookies on dashboard
|
||||
- JWT access tokens (15 minute expiration)
|
||||
- Refresh tokens (30 days) with rotation and revocation; a refresh token works only once
|
||||
- The dashboard keeps both tokens in `SameSite=Lax` cookies (`Secure` over HTTPS). They are readable by
|
||||
the dashboard's JavaScript, so an XSS bug would expose them; the short access token lifetime limits the damage
|
||||
|
||||
## Authorization
|
||||
|
||||
### Role-Based Access
|
||||
- `admin`: Full access including device management and revocation
|
||||
- `viewer`: Read-only access to tabs, devices, and commands
|
||||
- `admin`: Full access, including users, device pairing, planned customers and sending reminders
|
||||
- `viewer` (shown as staff): views tabs and commands and can close tabs
|
||||
|
||||
### Venue Isolation
|
||||
- Every user and device is scoped to exactly one venue
|
||||
@@ -34,9 +35,9 @@
|
||||
- Body size limits enforced
|
||||
|
||||
### Rate Limiting
|
||||
- Device sync: 1 request per 30 seconds per device
|
||||
- Command creation: 10 requests per minute per user
|
||||
- Login and device registration: 10 requests per minute per client
|
||||
- General: 100 requests per minute per client
|
||||
- Device sync and command acks are not rate limited (they are authenticated by the device token)
|
||||
|
||||
### Error Handling
|
||||
- Consistent error format never exposes internals
|
||||
|
||||
Reference in New Issue
Block a user