The Skill Bytes Backend service is the core server framework for the Skill Bytes educational platform. Skill Bytes is designed to help developers learn new programming concepts and maintain their programming abilities through daily coding challenges, preventing skill decay due to AI dependency.
Live Application: skill-bytes.netlify.app
We Are Also Looking For Contributors! If you want to join the Skill Bytes project read the Contributing section.
- Overview
- Architecture
- Technology Stack
- Database Schema
- API Documentation
- Authentication & Authorization
- Environment Variables
- Development Setup
- Production Deployment
- Scripts & Utilities
- Logging
- Related Repositories
- Contributing
Skill Bytes Backend is a Node.js/Express.js REST API server that provides:
- User Authentication: Secure user authentication via JWT tokens, and hashed passwords
- Daily Challenges: New challenges each day based on their preferences, pulled from a pool of challenges
- Admin Tooling: Admin tools to send messages, send challenges, and perform other tasks
- PostgreSQL Database: A PostgreSQL database, with Prisma ORM to connect the database to the Node.js backend. The backend is fully containerized using Docker and Docker Compose. NOTE: While the backend server does also handle serving the frontend, we will not discuss how the frontend works internally.
The backend follows a modular Express.js architecture:
server/
├── server.js # Main application entry point
├── routes/api # route handlers for all API endpoints
│ ├── auth/ # authentication endpoints
│ ├── user/ # user-protected endpoints
│ └── admin/ # admin-protected endpoints
├── middleware/ # express middleware
│ ├── authmiddleware.js # jwt authentication
│ └── adminmiddleware.js # admin password verification
├── prisma/ # database schema and migrations
├── scripts/ # utility scripts
├── public/ # static frontend assets
├── logger.js # log manager
├── dockerfile # docker build instructions
└── logs/ # application logs
Outside of the server directory, the project contains a docker-compose.yaml and docker-swarm.yaml. These files manage deploying all services. Read the COMPOSE.md and SWARM.md for more details.
the application runs three docker containers:
-
node.js/express server (
skill-bytes-server)- handles all http requests
- serves static frontend assets
- manages api routes and middleware
-
PostgreSQL database (
skill-bytes-db)- stores all application data
- managed via prisma orm
- persistent data volume
-
Cloudflared Tunnel (
cloudflared)
- creates a cloudflared tunnel to expose the application to the public network
- runtime: node.js 22 (alpine linux)
- framework: express.js 5.1.0
- database: postgresql 13 (alpine)
- orm: prisma 6.19.0
- authentication: jwt (jsonwebtoken)
- password hashing: bcryptjs
- logging: winston
- validation: validator.js
express: web framework@prisma/client: database clientjsonwebtoken: jwt token generation/verificationbcryptjs: password hashingcookie-parser: cookie parsing middlewarecors: cross-origin resource sharingwinston: logging frameworkvalidator: input validation
the application uses prisma orm with the following models:
The user model represents every account. A new user model is created with every request to the /api/auth/sign-up endpoint. The model contains three general attribute types:
- Identifiers such as the accounts id and email
- Account data such as the accounts preferences and name
- References to challenge models, such as the accounts previouslycompleted and openchallenge
model user {
id int @id @default(autoincrement())
email string @unique
passw string // hashed password
username string? @unique
fname string? // first name
createdat datetime @default(now())
inbox message[]
preferences string[] @default([]) // topic preferences
languages string @default("javascript")
previouslycompleted int[] @default([]) // challenge ids
completedchallenges int[] @default([]) // completed challenge ids
openchallenge challenge?
openchallengeid int? @unique
openchallengeupdatedat datetime?
points int @default(0)
}The challenge model represents a daily challenge. A new model is created with each request to the /api/admin/send-challenge endpoint. Each challenge contains two general attribute types:
- Identifiers, the only of which being the id
- Challenge data such as the challenges title and description
- References to the challenges owner
model challenge {
id int @id @default(autoincrement())
title string
description string
selectordescription string
difficulty string
content string
tags string[]
points int @default(0)
createdat datetime @default(now())
owner user?
ownerid int? @unique
testcases json? @default("[]")
generator json? @default("{}")
functionname string?
help string?
}Each message model represents a message sent to a user. A new model is created with each request to the /api/admin/send-message endpoint. Each message contains two general attribute types:
- Identifiers, the only of which being the id
- Message data such as the message's icon, content, and banner color
- References to the message's owner When created a message must be assigned to only one user, and is stored in the users inbox.
model message {
id int @id @default(autoincrement())
icon string @default("📢")
content string
bannercolor string @default("#2821fc")
owner user @relation(fields: [ownerid], references: [id])
ownerid int
}most endpoints require authentication via jwt cookies. see authentication & authorization for details.
This endpoint is used by the redirect site to ensure the backend is running before redirecting the user.
get /ping
response:
{
"msg": "pong"
}All auth endpoints use http basic authentication in the authorization header:
authorization: basic <base64(email:password)>
And all auth endpoints assign a jwt cookie to the response
post /auth/signup
request:
- basic auth:
email:password - email must be valid format
- password must be at least 6 characters
response:
201: user created, jwt cookie set400: invalid input409: email already exists500: server error
response body:
{
"msg": "user created successfully"
}post /auth/login
request:
- basic auth:
email:passwordorusername:password - email domain must be:
gmail.com,yahoo.com, orproton.me - password must be at least 6 characters
response:
200: login successful, jwt cookie set400: invalid input401: incorrect credentials404: user not found500: server error
response body:
{
"msg": "login successful"
}all user endpoints require jwt authentication. the jwt cookie is automatically sent by the browser, and is.
post /user/set-name
request body:
{
"name": "john doe"
}response:
200: name updated400: invalid input500: server error
post /user/set-username
request body:
{
"username": "johndoe123"
}validation:
- 3+ characters
- alphanumeric and underscores only
- must be unique
response:
200: username updated400: invalid input409: username taken500: server error
post /user/set-pref
request body:
{
"topics": ["arrays", "strings", "algorithms"]
}response:
200: preferences updated400: invalid input500: server error
post /user/set-pref-lang
request body:
{
"language": "javascript"
}supported languages: javascript
response:
200: language preference updated400: invalid language500: server error
get /user/inbox
response:
{
"messages": [
{
"id": 1,
"icon": "📢",
"content": "welcome to skill bytes!",
"bannercolor": "#2821fc",
"ownerid": 1
}
]
}delete /user/msg:msgid
response:
200: message deleted400: invalid message id404: message not found500: server error
get /user/get-daily-challenge
behavior:
- returns the same challenge if already fetched today
- otherwise selects a new challenge based on user preferences
- excludes previously completed challenges
response:
{
"challenge": {
"id": 1,
"title": "reverse string",
"description": "reverse a given string",
"selectordescription": "reverse a string",
"difficulty": "easy",
"content": "write a function...",
"tags": ["strings", "arrays"],
"points": 10,
"functionname": "reversestring",
"testcases": [...],
"generator": {...},
"help": "hint text"
}
}get /user/get-completed
response:
{
"challenges": [
{
"id": 1,
"title": "reverse string",
...
}
]
}post /user/complete-challenge
request body:
{
"code": "function reversestring(str) { ... }",
"challengeid": 1
}response:
200: challenge marked as completed, points awarded400: invalid input403: user not found404: challenge not found500: server error
get /user/challenge-completion-status
response:
{
"completedchallenges": [1, 2, 3]
}get /user/leader-board
response:
{
"leaderboard": [
{
"id": 1,
"username": "johndoe",
"points": 150
}
],
"id": 1 // current user id
}all admin endpoints require http basic authentication with admin password.
post /admin/send-msg
request body:
{
"content": "system maintenance scheduled",
"icon": "⚠️",
"bannercolor": "#ff0000"
}default values:
icon:"📢"bannercolor:"#2821fc"
response:
200: message sent to all users400: invalid input500: server error
post /admin/send-challenge
request body:
{
"title": "reverse string",
"description": "reverse a given string",
"selectordescription": "reverse a string",
"difficulty": "easy",
"tags": ["strings", "arrays"],
"content": "write a function that reverses a string...",
"functionname": "reversestring",
"testcases": [...],
"generator": {...},
"help": "hint: use array methods",
"points": 10
}response:
200: challenge created400: invalid input500: server error
the application uses jwt (json web tokens) stored in http-only cookies for session management.
- expiration: 24 hours
- cookie name:
jwt - cookie settings:
httponly: true (prevents javascript access)secure: true in production (https only)samesite:none(allows cross-origin)
- token payload:
{ "userid": 1, "email": "user@example.com" }
-
sign up / login:
- user provides credentials via basic auth
- server validates credentials
- server generates jwt token
- server sets jwt cookie in response
- browser automatically includes cookie in subsequent requests
-
protected endpoints:
- request includes jwt cookie
authmiddlewareverifies token- if valid, extracts
useridand attaches toreq.userid - request proceeds to route handler
-
token expiration:
- after 24 hours, token expires
- user must log in again
admin endpoints use http basic authentication with a password hash stored in environment variables.
- request includes
authorization: basic <credentials>header - middleware extracts password from basic auth
- password is compared against
admin_passw_hash(bcrypt) - if valid, request proceeds; otherwise returns
401
use the provided script:
node server/scripts/gen-passw-hash.js <your-password>the output is a base64-encoded hash that should be set as admin_passw_hash in your .env file.
create a .env file in the server/ directory with the following variables:
# database - vars auto assigned in docker-compose.yaml
# database_url=postgresql://postgres:postgres@skill-bytes-db:5432/skill-bytes?schema=public
# server - vars auto assigned in docker-compose.yaml
# port=3000
# node_env=production # or "development"
# authentication
jwt_secret=your-secret-key-here # use a strong, random string
# admin
admin_passw_hash=base64-encoded-bcrypt-hash # generate using gen-passw-hash.js- database_url: postgresql connection string (format:
postgresql://user:password@host:port/database?schema=public) - port: server port (default: 3000)
- node_env: environment mode (
productionordevelopment) - jwt_secret: secret key for signing jwt tokens (use a strong random string)
- admin_passw_hash: base64-encoded bcrypt hash of admin password
jwt secret:
# linux/mac
openssl rand -base64 32
# powershell (windows)
[convert]::tobase64string((1..32 | foreach-object { get-random -minimum 0 -maximum 256 }))admin password hash:
node server/scripts/gen-passw-hash.js your-admin-password- docker and docker compose installed
- node.js 22+ (for local development, optional)
- git
-
clone the repository
git clone <repository-url> cd skill-bytes-backend
-
create environment file
cd server cp .env.example .env # or create .env manually # edit .env with your configuration cd ..
-
start server
- Go to COMPOSE.md for details on developement hosting
- Go to SWARM.md for details on production hosting
-
verify setup
curl http://localhost:3000/ping # should return: {"msg":"pong"}
- database migrations: run migrations inside the container:
docker compose exec server npx prisma migrate dev --name migration-name - view compose logs: you can view logs from each service:
docker logs <SERVICE_NAME>
- view logs: logs are written to
server/logs/in development mode - stopping the service:
docker compose down
- removing volumes (clears database):
docker compose down -v
- security: ensure
jwt_secretandadmin_passw_hashare strong and kept secret - database backups: implement regular postgresql backups
- logging: in production, logs go to console (stdout/stderr) for container log aggregation
- monitoring: monitor the tmux session and docker containers regularly
- updates: pull latest changes and restart the tmux session for updates
- tmole stability: the watcher script automatically restarts tmole if it crashes
~/
├── skill-bytes-backend/ # this repository
├── skill-bytes-redirect/ # redirect service (auto-updated)
└── skill-bytes-frontend/ # frontend repo (auto-updated)
location: server/scripts/gen-passw-hash.js
generates a bcrypt hash for admin passwords.
usage:
node server/scripts/gen-passw-hash.js <password>output: base64-encoded hash (set as admin_passw_hash)
location: server/scripts/clear-logs.ps1
powershell script to clear log files (windows).
the application uses winston for structured logging.
- info: general information
- error: error conditions
development mode:
- console output
- file output:
server/logs/info.log: info-level logsserver/logs/error.log: error-level logs
production mode:
- console output only (stdout/stderr)
- logs are captured by docker and can be aggregated by log management tools
logs are in json format with timestamp:
{
"level": "error",
"message": "error in login",
"timestamp": "2024-01-01t12:00:00.000z",
"error": {...},
"stack": "..."
}development:
# view info logs
tail -f server/logs/info.log
# view error logs
tail -f server/logs/error.logproduction (docker):
# view server logs
docker compose logs -f server
# view database logs
docker compose logs -f skill-bytes-dbfrontend react application served statically from this backend's /public/dist directory.
note: in production, the frontend is typically deployed separately to netlify, but static assets can also be served from this backend.
redirect service that provides the current backend url to the frontend. updated automatically by tmole_watcher.sh in production.
please refer to the following documentation files:
- contributing.md: contribution guidelines
- cla.md: contributor license agreement
- agile.md: agile development practices
- license.md: license information
- read
contributing.md - create a feature branch
- make your changes
- test locally with
./boot.sh - update documentation if needed
- submit a pull request
database connection errors
- verify
database_urlin.env - ensure database container is running:
docker compose ps - check database logs:
docker compose logs skill-bytes-db
jwt authentication failing
- verify
jwt_secretis set in.env - check cookie settings match your domain
- ensure cookies are being sent (check browser devtools)
admin endpoints returning 401
- verify
admin_passw_hashis correctly set (base64-encoded) - regenerate hash using
gen-passw-hash.js - check basic auth header format
prisma migration errors
- reset database:
docker compose down -vthen rebuild - check migration files in
server/prisma/migrations/ - verify database schema matches prisma schema
port already in use
- change
portin.envordocker-compose.yaml - or stop the process using port 3000
tmole not working in production
- verify tmole is installed:
which tmole - check tmux pane 1 for error messages
- restart the tmux session
see license.md for license information.
for issues, questions, or contributions, please refer to the contributing guidelines or open an issue in the repository.
last updated: 2024