A RESTful user authentication and management API built with Node.js, Express 5, and PostgreSQL (via Neon). It provides secure user registration, login, logout, and full CRUD user management with role-based access control (RBAC).
Acquisition API is a production-ready backend service that handles the complete user lifecycle β from account creation with hashed passwords to JWT-based authentication and role-protected endpoints. Admins can manage all users while regular users can only access and modify their own accounts. The API is secured with Helmet, CORS, Arcjet rate limiting, and structured logging via Winston. It ships with Docker support for both local development (using Neon Local ephemeral database branches) and production deployments.
- User Authentication β Sign up, sign in, and sign out with JWT tokens stored in HTTP-only cookies
- Password Hashing β Secure password storage using bcrypt
- Role-Based Access Control β
adminanduserroles with middleware-enforced permissions - Soft Delete β Users are soft-deleted via a
deleted_attimestamp instead of being permanently removed - Pagination β Paginated user listing with configurable
pageandlimitquery parameters - Input Validation β Request validation powered by Zod schemas
- Configurable CORS β Origin allowlist controlled via the
CORS_ORIGINenvironment variable - Security Hardening β Helmet for HTTP headers, Arcjet for bot detection, shield protection, and role-based rate limiting (admin: 20/min, user: 10/min, guest: 5/min)
- Error Handling β Centralized 404 handler and global error handler with environment-aware error messages
- Structured Logging β Winston logger with file and console transports
- Database Migrations β Drizzle ORM with migration generation and push support
- Docker Support β Multi-stage Dockerfile with development and production Compose configurations
- CI/CD β GitHub Actions workflows for tests, linting, formatting, and Docker image builds
| Category | Technology |
|---|---|
| Runtime | Node.js 20 |
| Framework | Express 5 |
| Language | JavaScript (ES Modules) |
| Database | PostgreSQL via Neon |
| ORM | Drizzle ORM |
| Authentication | JSON Web Tokens (jsonwebtoken) + bcrypt |
| Validation | Zod |
| Security | Helmet, CORS, Arcjet (rate limiting, bot detection, shield) |
| Logging | Winston + Morgan |
| Testing | Jest + Supertest |
| Code Quality | ESLint + Prettier |
| Containerization | Docker + Docker Compose |
The diagram below shows how the major components of the system interact:
graph TD
Client["π Client\n(Browser / Mobile / Postman)"]
subgraph API["Acquisition API (Express 5)"]
direction TB
MW["Security Middleware\n(Helmet Β· CORS Β· Arcjet Β· Morgan)"]
Router["Router\n/api/auth Β· /api/users Β· /api/acquisition"]
AuthMW["Auth Middleware\n(JWT verify Β· Role check)"]
Controllers["Controllers\n(auth Β· user)"]
Services["Services\n(auth Β· user)"]
Utils["Utilities\n(jwt Β· cookies Β· format)"]
end
DB[("π PostgreSQL\n(Neon Cloud)")]
Logger["π Winston Logger\n(console + file)"]
Client -->|HTTP Request| MW
MW --> Router
Router -->|protected routes| AuthMW
AuthMW --> Controllers
Router -->|public routes| Controllers
Controllers --> Services
Services --> Utils
Services -->|Drizzle ORM| DB
Controllers -->|response| Client
API -->|logs| Logger
Every HTTP request passes through a layered middleware pipeline before a response is returned:
sequenceDiagram
participant C as Client
participant H as Helmet / CORS
participant A as Arcjet Security
participant V as Zod Validation
participant AM as Auth Middleware
participant Ctrl as Controller
participant Svc as Service
participant DB as PostgreSQL
C->>H: HTTP Request
H->>A: Headers sanitized
A-->>C: 429 Too Many Requests (rate-limited)
A->>V: Request allowed
V-->>C: 400 Bad Request (invalid body)
V->>AM: Body validated
AM-->>C: 401 Unauthorized (missing/invalid JWT)
AM-->>C: 403 Forbidden (insufficient role)
AM->>Ctrl: Token decoded β req.user set
Ctrl->>Svc: Call service method
Svc->>DB: Query via Drizzle ORM
DB-->>Svc: Result
Svc-->>Ctrl: Processed data
Ctrl-->>C: JSON Response (2xx / 4xx / 5xx)
flowchart TD
A([POST /api/auth/sign-up]) --> B{Body valid?\nZod schema}
B -- No --> C[400 Bad Request]
B -- Yes --> D{Email already\nregistered?}
D -- Yes --> E[409 Conflict]
D -- No --> F[Hash password\nbcrypt Γ 10 rounds]
F --> G[INSERT user row\nDrizzle ORM]
G --> H[Sign JWT token]
H --> I[Set HTTP-only cookie]
I --> J[201 Created + user data]
flowchart TD
A([POST /api/auth/sign-in]) --> B{Body valid?\nZod schema}
B -- No --> C[400 Bad Request]
B -- Yes --> D[Fetch user by email]
D --> E{User found?}
E -- No --> F[401 Unauthorized]
E -- Yes --> G{Password matches?\nbcrypt.compare}
G -- No --> F
G -- Yes --> H[Sign JWT token]
H --> I[Set HTTP-only cookie]
I --> J[200 OK + user data]
graph LR
subgraph Roles
Admin["π admin"]
User["π€ user"]
Guest["π guest\n(unauthenticated)"]
end
subgraph Endpoints
SignUp["POST /api/auth/sign-up"]
SignIn["POST /api/auth/sign-in"]
SignOut["POST /api/auth/sign-out"]
ListUsers["GET /api/users/"]
GetUser["GET /api/users/:id"]
UpdateUser["PUT /api/users/:id"]
DeleteUser["DELETE /api/users/:id"]
end
Guest -->|allowed| SignUp
Guest -->|allowed| SignIn
Admin -->|allowed| SignOut
User -->|allowed| SignOut
Admin -->|allowed| ListUsers
Admin -->|allowed| GetUser
User -->|own ID only| GetUser
Admin -->|allowed| UpdateUser
User -->|own ID only| UpdateUser
Admin -->|allowed| DeleteUser
User -->|own ID only| DeleteUser
The application uses a single users table managed by Drizzle ORM:
erDiagram
USERS {
serial id PK "Auto-increment primary key"
varchar255 name "Full name β not null"
varchar255 email "Unique email β not null"
varchar255 password "bcrypt hash β not null"
varchar50 role "admin | user β default user"
timestamp created_at "Row creation time"
timestamp updated_at "Last update time"
timestamp deleted_at "NULL = active (soft delete)"
}
| Column | Type | Constraints |
|---|---|---|
id |
serial | Primary key |
name |
varchar(255) | Not null |
email |
varchar(255) | Not null, unique |
password |
varchar(255) | Not null (hashed) |
role |
varchar(50) | Not null, default user |
created_at |
timestamp | Not null, default now |
updated_at |
timestamp | Not null, default now |
deleted_at |
timestamp | Nullable (soft delete) |
flowchart LR
Push["git push / PR"] --> T["β
tests.yml\nJest + Supertest"]
Push --> L["π lint-and-format.yml\nESLint + Prettier"]
Push --> D["π³ docker-build-and-push.yml\nBuild & push image"]
T -->|pass| Merge["Merge ready"]
L -->|pass| Merge
D -->|pass| Merge
| Workflow | Trigger | Description |
|---|---|---|
tests.yml |
Push / PR | Runs the test suite |
lint-and-format.yml |
Push / PR | Checks ESLint and Prettier formatting |
docker-build-and-push.yml |
Push / PR | Builds and pushes Docker images |
acquisition/
βββ src/
β βββ index.js # Entry point β starts the server
β βββ app.js # Express app setup (middleware, routes)
β βββ server.js # Alternate entry point (no dotenv)
β βββ config/
β β βββ database.js # Drizzle + Neon database connection
β β βββ logger.js # Winston logger configuration
β β βββ arcjet.js # Arcjet rate limiting configuration
β βββ models/
β β βββ user.model.js # User table schema (Drizzle)
β βββ controllers/
β β βββ auth.controller.js # Sign-up, sign-in, sign-out handlers
β β βββ user.controller.js # User CRUD handlers
β βββ routes/
β β βββ Auth.routes.js # Authentication routes
β β βββ user.routes.js # User management routes
β βββ services/
β β βββ auth.service.js # Authentication business logic
β β βββ user.service.js # User data operations
β βββ middleware/
β β βββ auth.middleware.js # JWT verification and role checking
β β βββ security.middleware.js# Security enforcement
β βββ validations/
β β βββ auth.validation.js # Zod schemas for auth endpoints
β β βββ users.validation.js # Zod schemas for user endpoints
β βββ utils/
β βββ jwt.js # JWT sign and verify helpers
β βββ cookies.js # Cookie set and clear helpers
β βββ format.js # Validation error formatting
βββ test/
β βββ app.test.js # API integration tests
βββ drizzle/ # Generated database migrations
βββ scripts/ # Docker and deployment scripts
βββ Dockerfile # Multi-stage production image
βββ docker-compose.dev.yml # Development environment (Neon Local)
βββ docker-compose.prod.yml # Production environment (Neon Cloud)
βββ drizzle.config.js # Drizzle Kit configuration
βββ jest.config.mjs # Jest test configuration
βββ eslint.config.js # ESLint rules
βββ .prettierrc # Prettier formatting rules
βββ DOCKER.md # Detailed Docker setup guide
βββ package.json # Dependencies and npm scripts
- Node.js v20 or later
- npm
- A Neon PostgreSQL database (or any PostgreSQL instance)
- Docker and Docker Compose (optional, for containerized setup)
git clone https://github.com/Dead-WaRior/acquisition.git
cd acquisitionnpm installCreate a .env file in the project root:
PORT=3000
NODE_ENV=development
LOG_LEVEL=info
DATABASE_URL=postgresql://<user>:<password>@<host>/<database>?sslmode=require
ARCJET_KEY=<your-arcjet-key>
JWT_SECRET=<your-jwt-secret>
CORS_ORIGIN=http://localhost:3000npm run db:pushnpm run devThe API will be available at http://localhost:3000.
| Variable | Description | Default |
|---|---|---|
PORT |
Port the server listens on | 3000 |
NODE_ENV |
Environment (development/production) |
development |
LOG_LEVEL |
Winston log level | info |
DATABASE_URL |
PostgreSQL connection string | β |
ARCJET_KEY |
Arcjet API key for rate limiting | β |
JWT_SECRET |
Secret key for signing JWT tokens | β |
CORS_ORIGIN |
Comma-separated list of allowed origins | β (allow all when empty) |
For Docker-specific environment variables, see DOCKER.md.
| Method | Endpoint | Description |
|---|---|---|
| GET | / |
Welcome message |
| GET | /health |
Health check with uptime |
| GET | /api |
API status |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/auth/sign-up |
Register a new user |
| POST | /api/auth/sign-in |
Log in and receive a JWT |
| POST | /api/auth/sign-out |
Log out and clear the token |
| Method | Endpoint | Access | Description |
|---|---|---|---|
| GET | /api/users/ |
Admin only | Fetch all users (supports ?page=1&limit=10) |
| GET | /api/users/:id |
Admin or self | Get user by ID |
| PUT | /api/users/:id |
Admin or self | Update user |
| DELETE | /api/users/:id |
Admin or self | Soft-delete user |
Protected endpoints require a valid JWT token sent via an HTTP-only cookie or an
Authorization: Bearer <token>header.
| Command | Description |
|---|---|
npm run dev |
Start the server in watch mode |
npm start |
Start the server |
npm test |
Run tests with coverage |
npm run lint |
Check code with ESLint |
npm run lint:fix |
Auto-fix ESLint issues |
npm run format |
Format code with Prettier |
npm run format:check |
Check code formatting |
npm run db:generate |
Generate database migrations |
npm run db:push |
Apply database migrations |
npm run db:studio |
Open Drizzle Studio (database GUI) |
npm run dev:docker |
Start development Docker environment |
npm run prod:docker |
Start production Docker environment |
The project ships with Docker support for both development and production. See DOCKER.md for the full guide.
Quick start (development):
docker compose -f docker-compose.dev.yml up --buildQuick start (production):
docker compose -f docker-compose.prod.yml up --build -dTests are written with Jest and Supertest:
npm testThis runs the test suite and generates a coverage report.
This project is licensed under the ISC License.