A self-hosted random image API with an admin panel, user portal, and folder-based media catalog. Serve curated images from sfw, nsfw, and furry categories; API access is gated by per-user keys, while image URLs are public once returned.
- Random image API —
GET /api/v1/imagereturns a random image for a category and type - Type discovery —
GET /api/v1/typeslists available types within a category - Public image delivery — images are served at
/i/{category}/{type}/{filename} - User portal — request access, log in, and manage API keys from the dashboard
- Admin panel — approve access requests, manage users, and view API keys
- OpenAPI docs — interactive documentation at
/docs(Scalar UI) - ALTCHA spam protection — bot-resistant access request form
- Discord notifications — optional webhook alerts for new access requests
- Node.js 20 or later
- MariaDB 11 (or use the included Docker Compose stack)
- Docker and Docker Compose (optional, recommended for production)
The fastest way to run the full stack (app + MariaDB):
# 1. Clone and enter the project
git clone <repo-url>
cd ImageAPI
# 2. Create and populate .env with generated secrets
cp .env.example .env
npm run generate:env
# 3. Add your media files (see Media Layout below)
mkdir -p media/sfw media/nsfw media/furry
# 4. Start the stack
npm run docker:upThe app will be available at http://localhost:3000. MariaDB is exposed on port 3307 on the host (mapped from container port 3306).
On first startup, the server runs database migrations and creates the admin account using ADMIN_EMAIL and ADMIN_PASSWORD from .env. Save the generated values printed by generate:env.
To stop the stack:
npm run docker:downFor development without Docker, run MariaDB locally and point the app at it.
npm installcp .env.example .env
npm run generate:envEdit .env for local development:
| Variable | Docker value | Local dev value |
|---|---|---|
DATABASE_HOST |
db |
localhost |
DATABASE_PORT |
3306 |
3306 (or your MariaDB port) |
BASE_URL |
http://localhost:3000 |
same |
MEDIA_ROOT |
./media |
./media |
Create the database and user in MariaDB to match MARIADB_* values in .env:
CREATE DATABASE imageapi;
CREATE USER 'imageapi'@'localhost' IDENTIFIED BY 'your-password';
GRANT ALL PRIVILEGES ON imageapi.* TO 'imageapi'@'localhost';
FLUSH PRIVILEGES;Tailwind CSS must be compiled before the UI will look correct:
npm run build:cssRe-run this after editing files in src/styles/.
Place image files under media/ (see Media Layout). Supported formats: .gif, .jpg, .jpeg, .png, .webp.
# Development with auto-reload
npm run dev
# Production-style start
npm startThe server waits for the database, runs migrations, bootstraps the admin user, and scans the media catalog on startup.
Copy .env.example to .env and fill in values. Use npm run generate:env to auto-generate secrets:
npm run generate:env # Generate missing secrets
npm run generate:env -- --force # Regenerate all secrets
npm run generate:env -- --stdout # Print to stdout instead of writing .env| Variable | Description |
|---|---|
PORT |
HTTP port (default: 3000) |
BASE_URL |
Public base URL used in API responses (no trailing slash) |
SESSION_SECRET |
Secret for signing session cookies |
ALTCHA_HMAC_SECRET |
Secret for ALTCHA challenge verification |
MARIADB_ROOT_PASSWORD |
MariaDB root password (Docker only) |
MARIADB_DATABASE |
Database name (default: imageapi) |
MARIADB_USER |
Database user (default: imageapi) |
MARIADB_PASSWORD |
Database user password |
DATABASE_HOST |
Database hostname (db in Docker, localhost locally) |
DATABASE_PORT |
Database port (default: 3306) |
ADMIN_EMAIL |
Initial admin account email |
ADMIN_PASSWORD |
Initial admin password (auto-generated if empty on first boot) |
DISCORD_WEBHOOK_URL |
Optional Discord webhook for access request notifications |
MEDIA_ROOT |
Path to the media directory (default: ./media) |
Images are organized by category and type. The catalog scans subdirectories recursively:
media/
├── sfw/
│ ├── neko/
│ │ ├── image1.png
│ │ └── image2.gif
│ └── waifu/
│ └── ...
├── nsfw/
│ └── ...
└── furry/
└── ...
- Categories must be one of:
sfw,nsfw,furry - Types are folder names (or nested paths) under each category, e.g.
neko,blowjob/animated - The API
typeparameter maps to these folder paths
- Visit
/request-accessand submit the form (protected by ALTCHA) - An admin approves the request at
/admin - Log in at
/loginand copy your API key from the dashboard
All API routes require authentication via Authorization: Bearer <key> or X-API-Key: <key>.
Get a random image
GET /api/v1/image?category=sfw&type=neko
Response:
{
"category": "sfw",
"type": "neko",
"filename": "abc123.png",
"url": "http://localhost:3000/i/sfw/neko/abc123.png",
"contentType": "image/png"
}List types in a category
GET /api/v1/types?category=sfw
Response:
{
"category": "sfw",
"types": ["neko", "waifu"]
}Serve an image directly (no API key required)
GET /i/sfw/neko/abc123.png
Interactive docs with try-it-out support are available at /docs.
Log in with the admin credentials from .env and visit /admin to:
- Review and approve/reject access requests
- Enable or disable user accounts
- View user API keys
| Command | Description |
|---|---|
npm start |
Start the production server |
npm run dev |
Start with file watching |
npm run build:css |
Compile Tailwind CSS |
npm run generate:env |
Generate .env secrets |
npm run docker:up |
Build and start Docker Compose stack |
npm run docker:down |
Stop Docker Compose stack |
├── src/
│ ├── server/ # Fastify web server
│ │ ├── auth/ # Sessions, API keys, users
│ │ ├── db/ # MariaDB pool, migrations, schema
│ │ ├── media/ # Folder catalog scanner
│ │ ├── routes/ # API, pages, images, ALTCHA
│ │ └── notifications/
│ └── styles/ # Tailwind source CSS
├── views/ # Handlebars templates
├── public/ # Static assets (compiled CSS)
├── scripts/ # Utility scripts (generate-env)
├── media/ # Image files (gitignored)
├── docker-compose.yml
└── Dockerfile
Private — all rights reserved.