Bun + Hono API service with a static-export Next.js frontend for text-to-image generation, history, sharing, and a public gallery.
The backend is the source of truth for auth, generation jobs, quota checks, image storage, sharing, and likes. The frontend reads real API state and should not invent fallback success states.
- Auth-key based private workspaces through
POST /auth/register. - Text-to-image jobs that are persisted in PostgreSQL before processing.
- OpenAI-compatible image-provider integration through the configured provider base URL and bearer token.
- Runtime image storage under
assets/<two-character-bucket>/, with metadata and sharing state stored in PostgreSQL. - Private generation history, standalone image detail, public sharing, likes, and a shared image gallery.
- Static-export frontend deployment that can be hosted separately from the API.
The root public/ directory contains example screenshots from the product site.
They are README/demo resources, separate from generated runtime image assets in
assets/.
- Bun
- PostgreSQL
- Docker, only for API container deployment
bun install
bun install --cwd frontend
cp .env.example .env.devFill .env.dev before running local services or migrations:
DATABASE_URL=postgresql://localhost:5432/gpt_images
IMAGE_PROVIDER=packyapi
IMAGE_API_KEY=your-image-provider-api-key
IMAGE_BASE_URL=https://www.packyapi.com/v1
LOG_LEVEL=info
API_ORIGIN=http://localhost:3000
FRONTEND_PORT=3001DATABASE_URL is required by Drizzle and database health checks.
IMAGE_PROVIDER, IMAGE_API_KEY, and IMAGE_BASE_URL are required by the
public /images/generations job flow. The current provider id is packyapi,
which uses an OpenAI-compatible relay protocol with one API key and one base
URL.
The provider client calls ${IMAGE_BASE_URL}/images/generations with
Authorization: Bearer ${IMAGE_API_KEY} and expects a JSON response shaped like
{ "data": [{ "url": "https://..." }] }, with optional created metadata. The
backend downloads returned URLs into local storage and marks the job as failed
when the provider response is invalid.
API_ORIGIN controls the local Hono API origin. The API dev server derives its
listen port from this value. FRONTEND_PORT controls the local Next.js dev
server. Frontend dev builds receive NEXT_PUBLIC_API_ORIGIN from API_ORIGIN.
LOG_LEVEL is optional and defaults to info; valid values are debug,
info, warn, and error.
Run database migrations for local development:
bun run db:migrate:devStart only the API:
bun run devStart only the frontend:
bun run dev:frontendStart both with the same .env.dev contract:
bun run dev:allWith the default environment, the API listens on http://localhost:3000 and the
frontend listens on http://localhost:3001.
bun run dev
bun run dev:frontend
bun run dev:all
bun run test
bun run typecheck
bun run build:frontend
bun run typecheck:frontend
bun run db:generate
bun run db:migrate
bun run db:migrate:dev
bun run deploy:api:docker
bun run deploy:static-pages:prepareWhen both frontend verification commands are needed, run
bun run build:frontend before bun run typecheck:frontend so Next.js can
generate frontend/.next/types.
src/app.ts: Hono app composition, CORS, and route mounting.src/index.ts: Bun server entrypoint.src/config/env.ts: required and optional environment accessors.src/db/: Drizzle client, schema, and migration runner.src/routes/: health, auth, site-info, and image HTTP routes.src/services/images/: provider clients, generation service, job queue, repository, storage, and tests.scripts/: local dev, Docker deploy, and static-pages preparation scripts.frontend/: static-export Next.js workspace for the product UI.frontend/app/: App Router pages.frontend/features/: route-focused frontend implementation.frontend/components/: shared and UI primitives.frontend/lib/: frontend API clients, normalization, i18n, and helpers.drizzle/: generated SQL migrations.public/: product-site screenshots used as README/demo resources.assets/: local runtime image asset storage used by the API.CNAME: static-pages custom-domain file copied into the publish worktree.
Do not hand-edit generated output such as frontend/.next/, frontend/out/,
node_modules/, or generated Drizzle SQL unless you are intentionally updating
generated artifacts through the matching command.
/: text-to-image workbench./text-to-image: text-to-image workbench./text-to-image/history?id=<assetId>: standalone image detail page./gallery: shared image gallery./workbench: text-to-image workbench alias.
The frontend is static-export friendly. bun run build:frontend writes the
export to frontend/out/. When NEXT_PUBLIC_API_ORIGIN is set, API calls are
sent to that backend origin. When it is unset, frontend clients use same-origin
paths such as /auth/register, /site/info, and /images/generations.
General routes:
GET /GET /healthGET /health/dbGET /site/infoPOST /auth/register
Image routes:
POST /images/generationsPOST /images/jobs/batchGET /images/requests/recentGET /images/historyGET /images/history/:idGET /images/squareGET /images/assets/:assetIdGET /images/assets/:assetId/sharePOST /images/assets/:assetId/shareDELETE /images/assets/:assetId/sharePOST /images/assets/:assetId/likeDELETE /images/assets/:assetId/like
Routes that read or mutate a private workspace require the X-Auth-Key header.
The frontend stores this value in localStorage.auth_key after calling
POST /auth/register.
Shared image asset bytes and shared image detail can be read without auth.
GET /images/assets/:assetId only requires X-Auth-Key when the asset is not
shared. When X-Auth-Key is present on the shared detail route, the backend can
include viewer-specific state such as ownership or like status.
GET /site/info returns the current image-generation quota model. The default
site quota is 4000 successful or in-flight image-generation jobs.
POST /images/generations accepts JSON:
{
"prompt": "A product photo of a desk lamp on a steel table",
"size": "auto",
"quality": "auto"
}prompt is required. size defaults to auto and also accepts valid
WIDTHxHEIGHT values within the backend image constraints: width and height
must be multiples of 16, the longest side must be at most 3840, the aspect ratio
must be at most 3:1, and the total pixel count must be between 655360 and
8294400. quality defaults to auto and accepts low, medium, or high.
Generation jobs currently use the backend default model gpt-image-2.
Example API smoke flow:
AUTH_KEY="$(curl -s -X POST http://localhost:3000/auth/register | bun -e "const p = await new Response(Bun.stdin.stream()).json(); console.log(p.authKey)")"
curl -s http://localhost:3000/images/generations \
-H "Content-Type: application/json" \
-H "X-Auth-Key: ${AUTH_KEY}" \
-d '{"prompt":"A minimal studio product photo of a black ceramic cup","size":"auto","quality":"auto"}'The generation route creates a persisted job, enqueues the in-process worker,
and returns 202. Poll returned operation IDs with POST /images/jobs/batch.
POST /images/jobs/batch accepts JSON:
{
"operationIds": ["00000000-0000-0000-0000-000000000000"]
}GET /images/square returns shared gallery items for the current auth key. It
also accepts optional likeOrder and sharedAtOrder query parameters with
asc or desc values.
Generate migrations after schema changes:
bun run db:generateRun migrations with the current shell environment:
bun run db:migrateRun migrations with .env.dev:
bun run db:migrate:devThe Docker flow is API-only. It builds Dockerfile.api, runs the Hono service
from src/index.ts, and mounts a host assets/ directory to /app/assets so
generated image files survive container replacement.
Prepare a production env file before deploying. At minimum it must define:
DATABASE_URL=postgresql://db.example.com:5432/gpt_images
IMAGE_PROVIDER=packyapi
IMAGE_API_KEY=your-image-provider-api-key
IMAGE_BASE_URL=https://www.packyapi.com/v1
LOG_LEVEL=infoRun a deployment:
bun run deploy:api:docker -- --env-file .env --migrateUseful overrides:
HOST_PORT=8080 \
IMAGE_TAG=registry.example.com/gpt-images-api:2026-05-01 \
CONTAINER_NAME=gpt-images-api \
bun run deploy:api:docker -- --env-file .env --migrateThe deploy script fails on missing Docker, missing env keys, failed migrations,
failed container replacement, or a failed GET /health check.
The API writes newline-delimited JSON logs to stdout/stderr. Each request gets
an X-Request-ID response header, and image job logs include the related
operationId and provider request id when available. In Docker deployments,
inspect runtime logs with:
docker logs gpt-images-apiThe static pages flow is frontend-only. It builds the static Next.js export from
frontend/, copies the generated files into a static-pages worktree, and leaves
commit and push steps manual.
The root CNAME file is the source of truth for the pages domain:
your-static-site.example.comReplace the placeholder value before a real static-pages publish.
The script loads the root .env file before building. Put static frontend
public variables there when they should be included in the exported site:
API_ORIGIN=https://api.example.com
NEXT_PUBLIC_WECOM_URL=https://example.com/contactPrepare the publish worktree. --api-origin is still supported and overrides
API_ORIGIN from .env:
bun run deploy:static-pages:prepare -- --api-origin https://api.example.comThe script writes the prepared site to .worktrees/static-pages/. Review and
publish it manually:
cd .worktrees/static-pages
git status
git add -A
git commit -m "deploy: update static pages"
git push origin static-pagesBackend changes should normally pass:
bun run test
bun run typecheckFrontend changes should normally pass:
bun run build:frontend
bun run typecheck:frontendFor documentation-only changes, at minimum run a whitespace check on the touched files:
git diff --check -- README.md README.zh-CN.md