💼 Freelance Project — This API was developed for a client as a lookup service for the Mobile Legends community, allowing players to verify account IDs with a history of theft or fraud before making any trade. See it live at: blacklist.reicontas.com.br
Back-end & Infrastructure:
- Node.js + TypeScript — Solid, typed, and scalable foundation
- Express — API routing and middlewares
- Prisma ORM — Type-safe data modeling and queries
- Supabase (PostgreSQL) — Cloud-hosted relational database
- Docker — Full containerization for standardized deployment
- GitHub Actions — CI/CD pipeline for automated build and deploy
- ESLint + Prettier — Static analysis and consistent code style enforcement
/
├── .github/
│ └── workflows/
│ └── deploy.yml # CI/CD pipeline
├── prisma/
│ ├── migrations/ # Database migrations history
│ └── schema.prisma # Data models definition
├── public/ # Static assets directory (not served by the current app)
├── src/
│ ├── IDs/
│ │ ├── controller/
│ │ │ └── idController.ts # Request handlers
│ │ └── routes/
│ │ └── idRoutes.ts # Route definitions
│ ├── lib/
│ │ └── prisma.ts # Prisma client singleton
│ ├── middlewares/ # Custom middlewares (rate limit, etc.)
│ ├── app.ts # Express app setup
│ └── server.ts # HTTP server entry point
├── .dockerignore
├── .editorconfig
├── .env.example
├── .prettierrc
├── docker-compose.yml
├── Dockerfile
├── eslint.config.js
├── package.json
├── prisma.config.ts
├── tsconfig.json
└── yarn.lock
- ID management — Public lookup, individual or bulk creation, and deletion. There is no update route.
- Redis rate limiting — Independent quotas per IP: 10 lookups per minute and 5 administrative attempts per minute shared across all three write routes. Excess requests return
429; there is no additional five-minute cooldown. IPv6 addresses are grouped according to the library default. - Administrative authentication — Write routes require
x-api-key, compared withAPI_KEY_SECRETin constant time for equal-length keys. Rate limiting runs before authentication, so invalid attempts (401) also count. - Zod validation — Creation requires numeric IDs with at least four digits. Lookup and deletion extract the first numeric sequence and validate the result:
74694823 (1278)is accepted;abcdandabc1are rejected. - Trusted proxies —
TRUSTED_PROXIESdefines the proxy addresses Express trusts when interpreting forwarded headers. - CORS —
CLIENT_URLcontrols which origins may read responses in browsers. CORS does not replace authentication or block external HTTP clients.
| Method | Route | Authentication | Quota per minute |
|---|---|---|---|
| GET | /ids/:id |
Public | 10 per IP |
| POST | /ids/add |
x-api-key |
5 per IP, shared across administrative routes |
| POST | /ids/bulk |
x-api-key |
Same administrative quota |
| DELETE | /ids/:id |
x-api-key |
Same administrative quota |
Creation accepts { "id": "12345678", "server": 1234, "descricao": "example" }. server and descricao are optional; the server must be a four-digit integer and the description must contain at least four characters. /ids/bulk accepts an array of these objects. Keep the administrative key private.
Clone the repository and enter the project directory. Use .env.example as a starting point and complete .env with the variables below; the example file does not yet list all of them:
PORT=3333
DATABASE_URL="postgresql://user:password@postgres:5432/postgres"
REDIS_URL="redis://redis:6379"
API_KEY_SECRET="replace-with-a-long-random-key"
CLIENT_URL="http://localhost:3000"
TRUSTED_PROXIES=PostgreSQL and Redis must be available at the configured addresses. postgres and redis are example hostnames and must resolve inside the container. The local Compose file does not create these services. API_KEY_SECRET is required; the API will not start without it. CLIENT_URL accepts comma-separated origins. Without REDIS_URL, the code tries redis://localhost:6379, which refers to the container itself when running in Docker.
docker compose up -d --buildThe repository Compose file builds the image locally and publishes 3333:3333. Prepare the database structure according to prisma/schema.prisma; starting the container does not automatically apply migrations.
Use the same image on different VPS instances and set TRUSTED_PROXIES in each API environment to the Nginx Proxy Manager IP on the shared network. Illustrative example:
TRUSTED_PROXIES=172.18.0.2This is the NPM IP, not the API IP entered as the forwarding destination in its dashboard. The application splits the variable by commas, removes whitespace and empty entries, and passes the list to trust proxy. If missing or empty, it uses false. This replaces one-hop trust (trust proxy = 1) with trust in the configured addresses.
NPM must forward visitor headers correctly. Keep the API port unpublished on the host and communicate through the shared Docker network. Ensure the NPM IP stays stable or update the variable when it changes; recreate the API container to apply environment changes.
Example VPS Compose file, separate from the development Compose file:
services:
api:
image: geovani3344/id-query-v20-api:latest
container_name: id-query
restart: always
env_file:
- .env
networks:
- rede-proxy
networks:
rede-proxy:
external: trueThe external network must exist and be shared with NPM. Adjust its name on each VPS and configure access to PostgreSQL and Redis.
To update manually, run these commands in that Compose directory:
docker compose pull api
docker compose up -d api
docker compose ps
docker compose logs --tail=50 apiThere is no need to run down first: the API remains active during the download, with a possible brief interruption when the container is recreated.
On every push to main, .github/workflows/deploy.yml builds and publishes <DOCKERHUB_USERNAME>/id-query-v20-api:latest. It then connects to the VPS over SSH, enters ~/id-query-v20-api, and runs docker compose pull followed by docker compose up -d. The remote Compose file must reference the published image and load the VPS .env file.
| Secret | Contents |
|---|---|
DOCKERHUB_USERNAME |
Docker Hub username |
DOCKERHUB_TOKEN |
Docker Hub access token |
SSH_HOST |
VPS IP or domain, without ssh, a protocol, or a username |
SSH_USER |
SSH username |
SSH_KEY |
Private key used to authenticate the deployment |
SSH_KNOWN_HOSTS |
VPS address and known public host key in known_hosts format |
From a trusted session on the VPS receiving deployments, retrieve its public host key:
cat /etc/ssh/ssh_host_ed25519_key.pubCreate SSH_KNOWN_HOSTS under Settings → Secrets and variables → Actions with a single line in this format:
VPS_ADDRESS ssh-ed25519 VPS_PUBLIC_KEY
Replace VPS_ADDRESS with the exact value of SSH_HOST and VPS_PUBLIC_KEY with the returned key. Do not include root@, quotes, or only the key without its address. Use only the .pub file. The current workflow uses the default SSH port, 22.
The workflow writes secrets through environment variables, restricts SSH file permissions, and uses StrictHostKeyChecking=yes. The connection fails if the identity is not recognized; it does not use ssh-keyscan to trust a key obtained during deployment. set -e stops remote commands on failure. Since downloading precedes recreation, a failed pull does not first take the API down.
These SSH secrets belong to Actions and are not included in the image. Other VPS instances can pull the same image manually with their own environment variables.
MIT © Geovani Rodrigues