A collaborative platform for software teams to report bugs, suggest features, and coordinate resolutions. Built with Express 5 and TypeScript, backed by PostgreSQL with raw SQL queries, and deployed on Vercel.
Replace with your deployed URL after deployment
https://internal-tech-issue-and-feature-tra.vercel.app
- User registration and login with role-based access (
contributor/maintainer) - JWT authentication using short-lived access tokens and long-lived refresh tokens
- HTTP-only cookie storage for both tokens
- Create, view, update, and delete issues (bugs and feature requests)
- Issue workflow status management (
open→in_progress→resolved) - Contributors can only update their own issues while status is
open - Maintainers can update or delete any issue and change any status
- Centralised global error handler with environment-aware responses (dev vs production)
- Auto-initialised PostgreSQL schema on server startup (no migration tool needed)
- Graceful shutdown on
unhandledRejectionanduncaughtException
| Layer | Package | Version |
|---|---|---|
| Framework | Express | ^5.2.1 |
| Language | TypeScript | ^6.0.3 |
| Database | PostgreSQL via pg |
^8.21.0 |
| Password hashing | bcrypt | ^6.0.0 |
| Auth tokens | jsonwebtoken | ^9.0.3 |
| Config / env | @dotenvx/dotenvx | ^1.66.0 |
| Cookies | cookie-parser | ^1.4.7 |
| CORS | cors | ^2.8.6 |
| Dev runner | tsx (watch mode) | ^4.22.3 |
| Bundler | tsup (ESM + CJS) | ^8.5.1 |
| Deployment | Vercel | — |
src/
├── app.ts # Express app: middleware, CORS, route mounting
├── server.ts # Entry point: DB init, server start, process safety nets
├── config/
│ └── index.ts # Centralised config from env vars
├── db/
│ └── index.ts # PostgreSQL Pool + auto schema init (CREATE TABLE IF NOT EXISTS)
├── middleware/
│ ├── auth.ts # authChecker (JWT guard), restrictTo (role guard)
│ └── index.d.ts # Express Request augmentation (req.user)
├── modules/
│ ├── auth/
│ │ ├── auth.d.ts # UserRole, JWTSignUser, AuthTokenType types
│ │ ├── auth.controller.ts # signup, login, refreshToken handlers
│ │ ├── auth.router.ts # POST /signup, /login, /refresh-token
│ │ └── auth.service.ts # DB queries, bcrypt, JWT sign/verify, cookie helpers
│ ├── issue/
│ │ ├── issue.d.ts # IssueType, IssueStatus types
│ │ ├── issue.controller.ts # CRUD + status update route handlers
│ │ ├── issue.router.ts # Issue routes with auth/role middleware
│ │ └── issue.service.ts # Raw SQL queries for all issue operations
│ └── error/
│ ├── error.ts # AppError class (operational vs non-operational)
│ └── error.controller.ts # Global error handler (dev: full stack, prod: safe message)
└── utils/
├── catch_async.ts # Wraps async handlers — eliminates try/catch boilerplate
└── send_response.ts # Unified JSON response helper
Tables are created automatically on server startup via initDB() using CREATE TABLE IF NOT EXISTS.
| Column | Type | Notes |
|---|---|---|
id |
SERIAL PRIMARY KEY |
Auto-increment |
name |
VARCHAR(255) |
Required |
email |
VARCHAR(255) UNIQUE |
Required, unique |
password |
VARCHAR(255) |
Hashed with bcrypt (12 salt rounds), never returned in responses |
role |
VARCHAR(30) |
contributor (default) or maintainer |
created_at |
TIMESTAMP |
Auto-set on insert |
updated_at |
TIMESTAMP |
Auto-set on insert |
| Column | Type | Notes |
|---|---|---|
id |
SERIAL PRIMARY KEY |
Auto-increment |
title |
VARCHAR(150) |
Required, max 150 chars |
description |
TEXT |
Required |
type |
VARCHAR(30) |
bug or feature_request |
status |
VARCHAR(30) |
open (default), in_progress, resolved |
reporter_id |
INTEGER |
References users(id) ON DELETE CASCADE |
created_at |
TIMESTAMP |
Auto-set on insert |
updated_at |
TIMESTAMP |
Auto-refreshed on update |
git clone <your-repo-url>
cd internal_tech_issue_and_feature_tracker_api
npm installCreate a .env file in the project root:
NODE_ENV=development
PORT=5000
# PostgreSQL connection string
DATABASE_URL=postgresql://user:password@host:5432/dbname
# JWT access token (short-lived)
JWT_SECRET=your_access_token_secret_here
JWT_ACCESS_TOKEN_EXPIRE=15m
# JWT refresh token (long-lived)
JWT_REFRESH_SECRET=your_refresh_token_secret_here
JWT_REFRESH_TOKEN_EXPIRE=7dThe app uses
@dotenvx/dotenvxto load.env. The schema is created automatically on first run — no migrations needed.
# Development — hot reload via tsx watch
npm run dev
# Build for production — outputs ESM + CJS to dist/
npm run build
# Start production server
npm startConfigured to accept requests from these local origins during development:
http://localhost:3000http://localhost:3001http://localhost:5173http://localhost:5174
Credentials (cookies) are enabled. Allowed methods: GET, POST, PUT, PATCH, DELETE.
Update the cors config in app.ts before deploying to production.
1. POST /api/auth/signup → Register account (contributor or maintainer)
2. POST /api/auth/login → Receive access_token (body + HTTP-only cookie)
+ refresh_token (HTTP-only cookie only)
3. Protected requests → Authorization: <access_token> (header, no "Bearer" prefix)
4. Token expired? → POST /api/auth/refresh-token
(refresh_token cookie sent automatically by browser)
→ New access_token issued in cookie + response body
Tokens are HTTP-only cookies (
secure: truein production,sameSite: lax). Passwords are hashed with bcrypt at 12 salt rounds and never returned in any response.
| Action | contributor |
maintainer |
|---|---|---|
| Register / Login | ✅ | ✅ |
| View all issues | ✅ | ✅ |
| View single issue | ✅ | ✅ |
| Create issue | ✅ | ✅ |
Update own issue (only if status is open) |
✅ | ✅ |
| Update any issue | ❌ | ✅ |
| Update issue status | ❌ | ✅ |
| Delete issue | ❌ | ✅ |
Base path: /api
Access: Public
Request body:
{
"name": "John Doe",
"email": "john.doe@devpulse.com",
"password": "securePassword123",
"role": "contributor"
}Valid role values: contributor, maintainer
Response 201:
{
"success": true,
"message": "User Created successfully!",
"data": {
"id": 1,
"name": "John Doe",
"email": "john.doe@devpulse.com",
"role": "contributor",
"created_at": "2026-01-20T09:00:00Z",
"updated_at": "2026-01-20T09:00:00Z"
}
}Access: Public
Request body:
{
"email": "john.doe@devpulse.com",
"password": "securePassword123"
}Response 200: Sets access_token and refresh_token as HTTP-only cookies. Also returns token in the response body.
{
"success": true,
"message": "User login successfully!",
"data": {
"token": "<access_token>",
"id": 1,
"name": "John Doe",
"email": "john.doe@devpulse.com",
"role": "contributor"
}
}Access: Public (reads refresh_token cookie automatically)
Response 200: Issues a new access_token cookie and returns the user payload.
| Method | Endpoint | Auth | Role | Description |
|---|---|---|---|---|
| GET | / |
No | — | Get all issues (with reporter info) |
| GET | /:id |
No | — | Get single issue (with reporter info) |
| POST | / |
Yes | contributor, maintainer | Create an issue |
| PATCH | /:id |
Yes | contributor(own issue), maintainer | Update an issue |
| DELETE | /:id |
Yes | maintainer |
Delete an issue |
| PATCH | /update-status/:id |
Yes | maintainer |
Update issue status |
*Contributors can only update their own issue if status is open. Maintainers can update any issue.
Auth header format:
Authorization: <access_token>
Returns all issues with the reporter's info embedded as a nested object.
Response 200:
{
"success": true,
"message": "Issues retrived successfully",
"data": [
{
"id": 45,
"title": "Database connection timeout under load",
"description": "Pool exhausts after 50+ concurrent queries",
"type": "bug",
"status": "open",
"reporter": {
"id": 1,
"name": "John Doe",
"role": "contributor"
},
"created_at": "2026-01-20T10:30:00Z",
"updated_at": "2026-01-20T10:30:00Z"
}
]
}Same shape as the array item above, wrapped in data: { ... }.
Request body:
{
"title": "Database connection timeout under load",
"description": "Pool exhausts after 50+ concurrent queries, causing 500 errors",
"type": "bug"
}Valid type values: bug, feature_request
Response 201:
{
"success": true,
"message": "Issue created successfully",
"data": {
"id": 45,
"title": "Database connection timeout under load",
"description": "Pool exhausts after 50+ concurrent queries, causing 500 errors",
"type": "bug",
"status": "open",
"reporter_id": 1,
"created_at": "2026-01-20T10:30:00Z",
"updated_at": "2026-01-20T10:30:00Z"
}
}All fields required.
Request body:
{
"title": "Updated title",
"description": "Updated description with steps to reproduce.",
"type": "bug"
}Maintainer only.
Request body:
{
"status": "in_progress"
}Valid status values: open, in_progress, resolved
Maintainer only. Returns 204 with no data on success.
{
"success": true,
"message": "Issue deleted successfully"
}All errors are processed through the global globalErrorController. The AppError class distinguishes between operational errors (known, expected) and programming errors (unexpected crashes).
NODE_ENV=development — full detail for debugging:
{
"status": "fail",
"error": {},
"message": "Issue not found",
"stack": "Error: Issue not found\n at ..."
}NODE_ENV=production — safe, minimal:
{
"success": false,
"status": "fail",
"message": "Issue not found"
}Non-operational errors in production always return a generic 500:
{
"status": "error",
"message": "something went very wrong!"
}| Code | Usage |
|---|---|
200 |
Successful GET, PATCH |
201 |
Resource created (POST) |
204 |
Successful DELETE (no body) |
400 |
Validation errors, duplicate email, invalid input |
401 |
Missing, expired, or invalid JWT |
403 |
Valid token but insufficient role/permission |
404 |
Resource not found |
500 |
Unexpected server or database error |
The project includes a vercel.json that routes all traffic to dist/server.js:
{
"version": 2,
"builds": [{ "src": "dist/server.js", "use": "@vercel/node" }],
"routes": [{ "src": "/(.*)", "dest": "dist/server.js" }]
}- Build the project:
npm run build - Push to GitHub
- Import the repo in Vercel
- Set all environment variables from the
.envsection in the Vercel dashboard - Deploy
Use NeonDB or Supabase for a hosted PostgreSQL instance compatible with Vercel's serverless environment.
tsup bundles src/server.ts into both ESM and CJS formats:
dist/
├── server.js # ESM build (used by Vercel)
├── server.cjs # CJS build
├── server.js.map
└── server.cjs.map