Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
JWT_SECRET=change-this-jwt-secret-for-local-dev
NEXT_PUBLIC_API_URL=http://localhost:3000
NEXT_PUBLIC_WS_URL=ws://localhost:3002/ws
WS_INTERNAL_URL=http://ws-server:3002
DATABASE_PATH=/data/notifications.db
WEB_PORT_INTERNAL=3001
WS_PORT=3002
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
node_modules/
.next/
.env
.env.local
*.db
data/
dist/
.turbo/
249 changes: 154 additions & 95 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,121 +1,180 @@
## Technical Challenge — Senior

### Background

A growing logistics company needs an internal tool to monitor delivery events across multiple warehouses in real time. Operations managers want to see live updates as packages move through different stages, receive alerts when something goes wrong, and review historical data.

In this challenge, you'll build a simplified version of this system: a real-time notification hub where events are created via API, stored in a database, broadcast to connected clients via WebSocket, and displayed in a simple dashboard.

We are **not evaluating specific tools or patterns**. We simply want to understand how you think, how you code, and how you approach real-world problems. Be yourself.


### What You Need to Build

A functional **full stack application** with:

1. A WebSocket server that broadcasts notifications to subscribed clients
2. A REST API to create and query notifications
3. A database to persist notification history
4. A UI that displays notifications in real time
5. Docker Compose to run the entire system


### Database Schema

Design the schema yourself. At minimum, you should be able to store:

- Notifications with: channel, title, message, priority, and timestamp
- Channels as a concept (either as a separate table or embedded — your call)

Include appropriate indexes for the queries your API supports.


### Tech Stack

#### Backend

* Runtime: **Bun**
* API Framework: **Next.js** (App Router)
* WebSocket Server: **Bun native WebSocket** (separate service)
* Database: **SQLite** (ORM, query builder, or raw SQL — your choice)

#### Frontend

* Framework: **Next.js**
* Styling: **TailwindCSS**
* Additional UI libraries are welcome but not required

#### Infrastructure

* **Docker** + **Docker Compose**


### Required API Endpoints
# Notification Hub — Real-Time Logistics Monitoring

A full-stack, real-time notification system for monitoring delivery events across multiple warehouses. Built with **Clean Architecture**, **SOLID principles**, **Repository pattern**, and **Dependency Injection**.

---

## Quick Start

```bash
cp .env.example .env
docker compose up --build
```

Open **http://localhost:3000** in your browser.

**Demo credentials:** `admin@example.com` / `password123`

JWT secret and runtime URLs are configured from the root `.env` file.

---

## Architecture Overview

```
Client -----> Next.js Web :3000
| |
| +----> SQLite (WAL)
|
+---- WebSocket -> WS Server :3002
|
+---- internal broadcast from API
```

### Services

| Service | Port | Technology | Responsibility |
|---------|------|------------|----------------|
| **Web** | 3000 | Next.js | REST API, SSR UI, business logic |
| **WS Server** | 3002 | Bun native WS | Real-time broadcasting, channel subscriptions |

### Clean Architecture Layers

```
src/
├── domain/ # Enterprise business rules
│ ├── entities/ # Notification, Channel, User
│ └── repositories/ # Repository interfaces (contracts)
├── application/ # Application business rules
│ ├── dtos.ts # Zod validation schemas
│ └── services/ # NotificationService, ChannelService, AuthService
├── infrastructure/ # Frameworks & drivers
│ ├── database/ # SQLite connection, migrations, seeds
│ ├── repositories/ # Concrete repository implementations
│ └── container.ts # IoC / Dependency Injection container
├── lib/ # Shared utilities (auth, API responses, helpers)
├── hooks/ # React hooks (useWebSocket)
├── contexts/ # React contexts (AuthContext)
├── components/ # UI components
└── app/ # Next.js App Router pages & API routes
```

**Dependency Rule:** Dependencies point inward. Domain has zero external dependencies. Infrastructure depends on domain interfaces through **Dependency Inversion**.

### SOLID Principles Applied

- **Single Responsibility:** Each service, repository, and component has a single, focused purpose.
- **Open/Closed:** New channels and priorities can be added without modifying existing code.
- **Liskov Substitution:** Repository interfaces can be swapped (e.g., SQLite for PostgreSQL) without breaking consumers.
- **Interface Segregation:** Separate repository interfaces for Notification, Channel, and User.
- **Dependency Inversion:** Services depend on abstractions (interfaces), not concrete implementations. The DI container wires implementations at boot time.

---

## Database Schema (SQLite + WAL)

```sql
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
created_at TEXT NOT NULL
);

CREATE TABLE channels (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
description TEXT,
created_at TEXT NOT NULL
);

CREATE TABLE notifications (
id TEXT PRIMARY KEY,
channel_id TEXT NOT NULL REFERENCES channels(id),
title TEXT NOT NULL,
message TEXT NOT NULL,
priority TEXT NOT NULL CHECK (priority IN ('low','medium','high','critical')),
created_at TEXT NOT NULL
);

-- Indexes for query performance
CREATE INDEX idx_notifications_channel_id ON notifications(channel_id);
CREATE INDEX idx_notifications_priority ON notifications(priority);
CREATE INDEX idx_notifications_created_at ON notifications(created_at DESC);
CREATE INDEX idx_notifications_channel_priority ON notifications(channel_id, priority);
```

---

## API Endpoints

| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/api/notifications` | Create a notification (channel, title, message, priority) |
| GET | `/api/notifications` | List notifications with filters (channel, priority, date range, limit/offset) |
| GET | `/api/channels` | List channels with notification count |

- Validate input. Return proper HTTP status codes.
- Use a consistent response structure.
| `POST` | `/api/notifications` | Create notification (channel, title, message, priority) |
| `GET` | `/api/notifications` | List notifications with filters (channel, priority, date range, limit/offset) |
| `GET` | `/api/channels` | List channels with notification count |
| `POST` | `/api/channels` | Create a new channel by name |
| `POST` | `/api/auth/login` | Authenticate user, returns JWT |
| `POST` | `/api/auth/register` | Create new account, returns JWT |

All responses follow a consistent envelope: `{ success: boolean, data?: T, error?: string, meta?: {} }`

### WebSocket Server
---

- Clients connect and subscribe to one or more channels.
- When a notification is created via the API, broadcast it to all clients subscribed to that channel.
- Expose a `GET /health` endpoint.
## WebSocket Protocol

**Connection:** `ws://localhost:3002/ws`

### Required UI
**Client messages:**

A single page that:
```json
{ "type": "subscribe", "channel": "warehouse-north" }
{ "type": "unsubscribe", "channel": "warehouse-north" }
{ "type": "subscribe_all" }
```

1. Connects to the WebSocket server
2. Shows a connection status indicator (connected/disconnected)
3. Displays notifications in real time as they arrive
4. Allows filtering by channel
**Server messages:**

```json
{ "type": "connected", "clientId": "uuid" }
{ "type": "subscribed", "channel": "warehouse-north" }
{ "type": "notification", "data": { ...notification } }
```

### Docker
Features: automatic reconnection with exponential backoff, channel re-subscription on reconnect, connection status indicator.

- `docker-compose.yml` that starts the WebSocket server, the Next.js app, and any other service needed.
- Must work with a single `docker compose up`.
---

## Key Design Decisions

### Submission Instructions
1. **SQLite with WAL mode:** Chosen for simplicity and zero-config deployment. WAL mode enables concurrent reads while writing, with `busy_timeout` for contention handling.

* **Fork this repository**, complete your work, and **submit a pull request**.
* Include a `README.md` with:
* Clear instructions to run the project locally
* A short explanation of your architecture and design decisions
* Any trade-offs or things you would improve with more time
2. **Bun-native WebSocket server as separate service:** Decouples real-time concerns from API logic. The API broadcasts to the WS server via internal HTTP, keeping the architecture simple and independently scalable.

3. **JWT authentication with `jose`:** Stateless auth, no session storage needed. Tokens set both in localStorage (for API calls) and as HTTP cookies (for Next.js middleware route protection).

### Time Expectation
4. **Zod for validation at the boundary:** Every API input is validated with Zod schemas at the route level. No validation logic leaks into domain or application layers.

You should spend no more than **3 hours** on this task.
5. **Framer Motion for UI animations:** Smooth, physics-based transitions for notifications arriving in real time, page transitions, and modal interactions.

Don't worry if you can't finish everything. What matters most is **how far you get** and **how you approach the problem**.
---

## Trade-offs & Improvements

### Evaluation & Guidance
Given more time, I would:

What we mainly evaluate:
- **Add unit and integration tests** for services and repositories.
- **Implement authentication on WebSocket connections** (pass JWT on connect, validate server-side).
- **Add pagination controls in the UI** — the API already supports `limit`/`offset` params, but the UI currently loads the latest 50 notifications without navigation controls.
- **Implement horizontal scaling** with Redis pub/sub for WS server coordination across instances.
- **Add structured logging** (Winston/Pino) with correlation IDs.
- **Implement database connection pooling** via `better-sqlite3-pool` if needed under high load.
- **Add health check to the web service** — the WS server already exposes `GET /health`, but the web service and `docker-compose.yml` healthcheck probes are pending.
- **CI/CD pipeline** with automated testing and Docker image publishing.

- Solution design and structure (architecture, modularity, separation of concerns).
- Clarity of reasoning and documentation (decisions, trade-offs, assumptions).
- Code quality (readability, consistency, error handling, good practices).
- WebSocket implementation (scalability, reconnection handling, channel management).
- Docker setup (networking, multi-service orchestration).
- Git workflow (incremental commits with clear messages).
- Prioritization and scope management: it's valid to leave items pending if you explain what and why.
---

Use of AI (optional but allowed):
## AI Usage Note

- You may use AI tools (e.g., Claude, Copilot, ChatGPT, Cursor) to assist with your solution.
- We care most about how you structure the solution and explain your decisions.
- If you used AI, add a brief note in your PR/README: which tools you used, which parts were assisted, and what changes you made after review.
- Only include code you understand and can justify.
GitHub Copilot (Claude) was used to assist in scaffolding the project structure, generating boilerplate code, and writing documentation. All code was reviewed, understood, and adjusted to ensure architectural coherence, correctness, and alignment with the challenge requirements.
42 changes: 42 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
services:
web:
build:
context: ./web
args:
JWT_SECRET: ${JWT_SECRET}
NEXT_PUBLIC_WS_URL: ${NEXT_PUBLIC_WS_URL:-ws://localhost:3002/ws}
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:3000}
ports:
- "3000:3001"
environment:
- PORT=${WEB_PORT_INTERNAL:-3001}
- DATABASE_PATH=${DATABASE_PATH:-/data/notifications.db}
- WS_INTERNAL_URL=${WS_INTERNAL_URL:-http://ws-server:3002}
- JWT_SECRET=${JWT_SECRET}
- NEXT_PUBLIC_WS_URL=${NEXT_PUBLIC_WS_URL:-ws://localhost:3002/ws}
- NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL:-http://localhost:3000}
volumes:
- sqlite-data:/data
depends_on:
ws-server:
condition: service_started
restart: unless-stopped
networks:
- app-network

ws-server:
build:
context: ./ws-server
ports:
- "${WS_PORT:-3002}:3002"
restart: unless-stopped
networks:
- app-network

volumes:
sqlite-data:
driver: local

networks:
app-network:
driver: bridge
4 changes: 4 additions & 0 deletions web/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
.next/
.env.local
*.db
24 changes: 24 additions & 0 deletions web/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
FROM node:20-bookworm-slim
WORKDIR /app

ARG JWT_SECRET
ARG NEXT_PUBLIC_WS_URL
ARG NEXT_PUBLIC_API_URL
ENV JWT_SECRET=${JWT_SECRET}
ENV NEXT_PUBLIC_WS_URL=${NEXT_PUBLIC_WS_URL}
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}

# better-sqlite3 needs node-gyp toolchain during install
RUN apt-get update \
&& apt-get install -y --no-install-recommends python3 make g++ \
&& rm -rf /var/lib/apt/lists/*

COPY package.json ./
RUN npm install
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
EXPOSE 3001
ENV PORT=3001
ENV NODE_ENV=production
CMD ["npm", "run", "start"]
5 changes: 5 additions & 0 deletions web/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
8 changes: 8 additions & 0 deletions web/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverComponentsExternalPackages: ["better-sqlite3"],
},
};

module.exports = nextConfig;
Loading