REST API for PayCraft, a payroll platform for employers: signup and onboarding, company and employee records, payroll runs, payouts, and virtual-account funding through KoraPay. This service exposes JSON APIs consumed by the PayCraft web app and integrates with KoraPay webhooks and optional USSD callbacks.
Architecture: see ARCHITECTURE.md for layers, security, data, and integrations.
- Architecture (detailed)
- Features
- Tech stack
- Prerequisites
- Configuration
- Local setup
- Database migrations (Flyway)
- Run the application
- API documentation
- Security model
- HTTP API overview
- Project layout
- Docker
- Background jobs
- Contributing
- Related projects
| Area | What the API supports |
|---|---|
| Authentication | Employer login, JWT access tokens, refresh token flow (/api/v1/auth). |
| Employers | Registration, profile read/update/delete, password change (/api/v1/employer). |
| Companies | Create (during onboarding), list, current company details, update, delete (/api/v1/company). |
| Employees | CRUD for employees linked to the employer/company context (/api/v1/employee). |
| Payroll | Create payrolls, add/remove employees, update/delete, list, run payroll (/api/v1/payroll). |
| Payments | Single-employee payout, bulk payout by payroll id, bank list, payment verification (api/v1/account/...). |
| Virtual account & funding | VBA details, Kora transactions, paginated payments, bank transfer instructions, card funding, transfer verification, saved cards (api/v1/account/...). |
| Webhooks | KoraPay server-to-server events with signature verification (POST /webhook). |
| USSD | Telecom-style callback for basic flows (/api/v1/ussd); intentionally omitted from Swagger. |
Cross-cutting behavior includes JPA auditing, global exception handling with a consistent error JSON shape, CORS for configured front-end origins, and Spring Boot Actuator for operational endpoints.
| Layer | Technologies |
|---|---|
| Runtime | Java 17 |
| Framework | Spring Boot 3.3.3 (Web, Data JPA, Security, Validation, Mail, Actuator) |
| Security | Spring Security 6, stateless sessions, custom JWT filter, JJWT 0.12.x |
| Persistence | Spring Data JPA, Hibernate; Flyway (MySQL migrations); MySQL (dev/prod); H2 for tests |
| Documentation | springdoc-openapi 2.6 (/swagger-ui, /v3/api-docs) |
| Templating | Thymeleaf (e.g. email or server-rendered content where used) |
| Build | Maven |
External integrations: KoraPay (payments, virtual accounts, webhooks), Resend (transactional email API).
- JDK 17 (Temurin or another distribution)
- Maven 3.8+
- MySQL 8 for the default dev profile
- Optional: Docker for container images
Spring loads application.yml and activates the profile from spring.profiles.active (default in repo: dev).
| Profile | Purpose | Server port (as configured) | Database |
|---|---|---|---|
dev |
Local development | 6020 (application-dev.yml) |
MySQL on localhost |
prod |
Production | 6090 (application-prod.yml) |
MySQL (host/credentials from env) |
There is no committed qa profile file in this repository; application-qa.yml is listed in .gitignore for local experiments (for example H2-only runs). You can add your own application-qa.yml if needed.
Create a .env file in the project root for tools that load it, and export the same variables in your shell (or configure your IDE) before running. Spring reads OS environment variables; variable names must match those referenced in the YAML.
Required for typical dev runs
| Variable | Used for |
|---|---|
DATABASE_NAME |
MySQL database name (default in dev: paycraft if unset in JDBC URL) |
DATABASE_USERNAME |
MySQL user |
DATABASE_PASSWORD |
MySQL password (dev) |
SECRET_STRING |
JWT HMAC secret as Base64 (see JWTService); must decode to sufficient key material |
KORA_SECRET_KEY |
KoraPay secret key |
KORA_PUBLIC_KEY |
KoraPay public key |
ENCRYPTION_KEY |
Application encryption material (see KoraPay / sensitive field usage) |
Production (prod)
| Variable | Notes |
|---|---|
DATABASE_PROD_PASSWORD |
Production DB password (application-prod.yml) |
| Same Kora/JWT/encryption vars as above |
Email (Resend)
| Variable | Purpose |
|---|---|
RESEND_API_KEY |
Resend API key used by EmailServiceImpl |
EMAIL_SENDER |
Verified Resend sender address (resend.from-email) |
Optional / defaults
| Variable | Purpose |
|---|---|
FRONTEND_URL |
CORS and links (default in base config: http://localhost:5173) |
WEBHOOK_URL |
Webhook base URL (dev has a default placeholder in application-dev.yml) |
WEBHOOK_DEV_URL |
Dev webhook URL override |
Never commit real secrets. Keep .env local (it is gitignored).
-
Clone the repository
git clone https://github.com/PayCraft-NG/PayCraft-Backend.git cd PayCraft-Backend -
Create the MySQL database (name should match
DATABASE_NAME, e.g.paycraft):CREATE DATABASE paycraft CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'paycraft'@'localhost' IDENTIFIED BY 'your_password'; GRANT ALL PRIVILEGES ON paycraft.* TO 'paycraft'@'localhost'; FLUSH PRIVILEGES;
-
Export environment variables (example; adjust values):
export DATABASE_NAME=paycraft export DATABASE_USERNAME=paycraft export DATABASE_PASSWORD=your_password # SECRET_STRING must be Base64 (e.g. openssl rand -base64 32) export SECRET_STRING="$(openssl rand -base64 32)" export KORA_SECRET_KEY=your_kora_secret export KORA_PUBLIC_KEY=your_kora_public export ENCRYPTION_KEY=your_32_char_compatible_key_material export RESEND_API_KEY=re_xxxxxxxxxxxxxxxxx export EMAIL_SENDER=onboarding@resend.dev export FRONTEND_URL=http://localhost:5173
-
First build
mvn clean verify
- Scripts: MySQL migrations in
src/main/resources/db/migration/mysql/and PostgreSQL migrations insrc/main/resources/db/migration/postgresql/. Profile-specific Flyway locations prevent cross-database version collisions. - Runtime: Flyway runs on startup before JPA; Hibernate
ddl-autoisvalidateso the live schema must match entities and applied migrations. - New database: empty schema → Flyway applies
V1and creates all tables. - Existing database (already created with older Hibernate
ddl-auto: update):spring.flyway.baseline-on-migrate: trueandbaseline-version: 1record the current state as version 1 without re-runningV1, so you avoid “table already exists” errors. AddV2__...sql(and later) for real changes going forward. - Tests: profile
testdisables Flyway and uses H2 withddl-auto: create-drop(seeapplication-test.yml).
To generate a new Base64 JWT secret locally:
openssl rand -base64 32# Default profile is dev (see application.yml)
mvn spring-boot:runOther useful forms:
# Explicit profile
export SPRING_PROFILES_ACTIVE=dev
mvn spring-boot:run
# After mvn package
java -jar target/paycraft-0.0.1-SNAPSHOT.jar- Dev: API base URL typically
http://localhost:6020 - Prod (when using
prodprofile): default port 6090 perapplication-prod.yml
When the app is running:
| Resource | URL (dev, port 6020) |
|---|---|
| Swagger UI | http://localhost:6020/swagger-ui/index.html |
| OpenAPI JSON | http://localhost:6020/v3/api-docs |
Use Authorize in Swagger with a Bearer token from POST /api/v1/auth/login.
Postman: import postman/PayCraft-Backend.postman_collection.json (collection variables baseUrl, employerId, employeeId, payrollId; login Tests script stores accessToken / refreshToken).
- JWT in the
Authorization: Bearer <token>header for protected routes. - Public (no JWT) routes include, among others:
POST /api/v1/auth/login,POST /api/v1/auth/refresh-tokenPOST /api/v1/employer/createPOST /api/v1/company/create(withemployerIdquery param)POST /webhook(KoraPay; validatesX-Korapay-Signature)api/v1/ussd(GET/POST)- Swagger and OpenAPI endpoints under
/swagger-ui/**and/v3/api-docs/**
- All other API paths require authentication.
Exact rules live in SecurityConfig.java.
Base paths are shown as implemented in controllers (some use a leading /, some rely on Spring’s path normalization).
| Area | Base path | Notes |
|---|---|---|
| Auth | /api/v1/auth |
Login, refresh |
| Employer | /api/v1/employer |
Signup + authenticated profile |
| Company | /api/v1/company |
Companies for the logged-in employer |
| Employee | api/v1/employee |
Employee CRUD |
| Payroll | api/v1/payroll |
Payroll lifecycle and run |
| Account / payments / VBA | api/v1/account and api/v1/account/ |
Payouts, banks, verify, VBA, transfers, cards |
| Webhook | /webhook |
KoraPay callback |
| USSD | /api/v1/ussd |
External USSD gateway callback |
src/main/java/com/aalto/paycraft/
├── api/ # USSD entrypoints
├── audit/ # JPA auditing
├── config/ # Security, OpenAPI, beans
├── constants/
├── controller/ # REST controllers
├── dto/
├── entity/
├── exception/
├── mapper/
├── repository/
└── service/ # Interfaces + implementations
src/main/resources/
├── application.yml
├── application-dev.yml
├── application-prod.yml
├── db/migration/ # Flyway versioned migrations
├── schema/ # Legacy sample SQL (superseded by Flyway)
└── templates/ # Thymeleaf templates
docker-compose.yml starts PostgreSQL 16 and the app with SPRING_PROFILES_ACTIVE=docker. The app uses JDBC to the postgres service, runs Flyway from classpath:db/migration/postgresql, and listens on 6020.
docker compose up --buildAPI: http://localhost:6020. Postgres is published on 5432 by default (override with POSTGRES_PORT).
Set real secrets via a .env file or your shell (at minimum SECRET_STRING as Base64, and Kora/email variables for full behavior). The compose file includes development-only placeholders so the stack can start without a .env.
The Dockerfile builds the JAR with Maven inside the image, then runs a slim JRE 17 image (no local mvn package required):
docker build -t paycraft-backend:local .To run that image against your own PostgreSQL (same env vars as in compose, plus SPRING_PROFILES_ACTIVE=docker and a reachable POSTGRES_HOST):
docker run --rm -p 6020:6020 \
-e SPRING_PROFILES_ACTIVE=docker \
-e POSTGRES_HOST=host.docker.internal \
-e DATABASE_USERNAME=paycraft \
-e DATABASE_PASSWORD=paycraft \
-e SECRET_STRING="$(openssl rand -base64 32)" \
paycraft-backend:localbuild_script.sh is an older flow that assumes a pre-built JAR in target/; prefer docker compose build or docker build above unless you still push to a registry from that script.
Automatic payrolls are driven by PayrollJobService: on startup it loads payrolls marked automatic from the database and schedules them with Spring’s TaskScheduler using each payroll’s cron expression. Creating or updating an automatic payroll can register or refresh that schedule.
application.yml contains a payroll.job.fixedRate property; it is not referenced by the current Java scheduling code—treat it as unused or reserved unless you wire it in later.
- Fork the repository and create a branch for your change.
- Run
mvn verifybefore opening a pull request. - Keep API changes documented via OpenAPI annotations and Swagger where appropriate.
- Open a pull request with a clear description of behavior and any new configuration.
- PayCraft Frontend — web client for this API.
This project is licensed under the terms in LICENSE.
PayCraft — payroll flows for employers, with KoraPay-backed funding and payouts.