A pixel-cat gacha site with generators, animated wheels, adoption tools, and a searchable Catdex.
- Cat Generator - Generate random pixel cats with customizable traits, accessories, tortie coats, and 20 derived coat patterns across gacha, builders, adoption, evolution, ancestry, streams, and Discord
- Gacha Wheel - Weighted wheel spins with animated reveals
- Catdex - Browse and search all generated cats in a Pokedex-style archive
- Adoption Generator - Roll whole litters, trim each round, finish with your favorites
- Visual Builder - Trait-by-trait sprite previews with instant updates
- Stream Tools - Live session controls for audience voting and shareable builds
- Frontend: Next.js 16, React 19, TailwindCSS
- Backend: PostgreSQL with Drizzle migrations, FastAPI renderer service
- Package Manager: pnpm
- Deployment: Kubernetes via FluxCD, Docker containers
- Node.js (v24+; required by Portless)
- pnpm (v10+)
- Access to the development PostgreSQL configuration in Infisical
- Python 3.11+ with uv (for renderer service)
- Infisical CLI with access to the development project
pnpm install --frozen-lockfile
pnpm --dir frontend install --frozen-lockfile --ignore-scripts
pnpm --dir backend/media_service install --frozen-lockfile
npm --prefix backend/discord-bot ci
pnpm --dir backend/image_processing_service install --frozen-lockfile
(cd backend/renderer_service && uv sync --frozen --extra dev)
pnpm devpnpm dev checks the generated cat-system contract and required secrets, starts
the frontend, renderer, media service, Discord bot, and palette watcher, then
opens the worktree-specific Portless URL after every service is ready. The exact
URL is printed in the terminal; it is intentionally not hard-coded.
The frontend uses PostgreSQL through the Drizzle data layer. In production, it
runs against the beastypage-postgres CNPG cluster in the webpage namespace,
with migrations in the frontend init container; production runtime values come
from Infisical paths /kubernetes/webpage/beastypage-postgres and
/kubernetes/webpage/beastypage-frontend-runtime-config. The local full
development setup instead injects the dev environment paths /frontend,
/media, and /discord-bot in the BeastyPage Infisical project through the
root scripts; see Local development notes.
pnpm --dir frontend run db:local up
pnpm --dir frontend run db:migrate
pnpm --dir frontend devBefore the final command, copy the DATABASE_URL line from the gitignored
frontend/.env.database.local into frontend/.env.local because Next.js does
not load the helper's filename. Add NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and
CLERK_SECRET_KEY there for signed-in flows. The root pnpm dev:frontend script
is the Infisical-backed alternative for injecting Clerk and service runtime
values; it does not provide DATABASE_URL.
The renderer service generates cat card images. If not running, the frontend will work but card generation will be unavailable.
cd backend/renderer_service
uv pip install --editable .
uv run uvicorn renderer_service.app.main:app --reload --port 8001| Variable | Description |
|---|---|
NEXT_PUBLIC_UMAMI_HOST |
Self-hosted Umami base URL |
NEXT_PUBLIC_UMAMI_WEBSITE_ID |
Umami website identifier |
NEXT_PUBLIC_UMAMI_DOMAINS |
Comma-separated allowed analytics hostnames |
NEXT_PUBLIC_GLITCHTIP_DSN |
GlitchTip public ingestion DSN |
NEXT_PUBLIC_APP_ENV |
production for version tags, ci otherwise; unset locally |
NEXT_PUBLIC_APP_VERSION |
Derived from APP_VERSION; release is beastypage@${APP_VERSION} |
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY |
Clerk browser key |
Analytics is opt-in through the cookie banner, including masked replay and
heatmaps. Error tracking uses GlitchTip without browser storage or user identity.
Both run only in production builds. Tag builds receive SENTRY_AUTH_TOKEN only
through a BuildKit secret for debug-ID artifact-bundle source-map uploads; maps
are deleted before packaging. See frontend telemetry documentation
for settings, the cookie and storage inventory and the isolated pnpm test:telemetry check.
The privacy policy lives at /datenschutz and /privacy.
| Variable | Description |
|---|---|
QUICK_SHARE_WORKER_URL |
Public base URL used to dispatch Quick Share jobs (for example, https://beastyrabbit.com) |
Quick Share background processing is event driven. See the Quick Share processing rollout and production runbook for the architecture, cost model, rollout lessons, and verification procedure.
| Variable | Description | Default |
|---|---|---|
PORT |
Server port | 3000 |
RENDERER_INTERNAL_URL |
Renderer service URL | - |
IMAGE_PROCESSING_INTERNAL_URL |
Image processing service URL | - |
MEDIA_SERVICE_INTERNAL_URL |
Media service URL | - |
beastypage/
├── frontend/ # Next.js application
│ ├── app/ # Next.js routes and PostgreSQL-backed API
│ └── Dockerfile # Multi-stage build (pnpm → Node)
├── backend/
│ ├── discord-bot/ # Discord commands and cat generation
│ ├── image_processing_service/
│ ├── media_service/ # Quick Share media worker
│ └── renderer_service/ # FastAPI cat renderer
├── deploy/ # PostgreSQL CNPG and Infisical sync templates
├── docs/ # Development, operations, migration and rollout docs
├── scripts/ # Sprite import tooling
└── .github/workflows/ # build-images.yml, validate.yml, sonarqube.yml
Each backend service directory has its own README with run commands and environment variables.
Additional operational notes:
- Stream overlay architecture and timing
- Local development notes
- PostgreSQL operations
- PostgreSQL architecture decision record
- PostgreSQL migration record
- Runtime TypeScript constraints
- Release verification
GitHub Actions builds and publishes GHCR images from .github/workflows/build-images.yml. Version-tag pushes such as v8.1.0 receive X.Y.Z, X.Y and commit SHA tags. Pushes to main build nothing, because Flux deploys semver tags only. Manual runs build the selected images and publish them under a smoke tag only when publish_images is set.
Source checks run once, in validate.yml on the pull request. A tag does not repeat them; its release-gate job proves instead that they passed for the exact tagged tree. The newest validate.yml run of the tag commit's pull request must have passed both applications and database and must have recorded the same Git tree, or a manual validate.yml run on the tag commit must have passed. Every image build, including manual publishing, needs that gate. After all five images are built, deploy-smoke waits for https://beastyrabbit.com/api/version to report the tag, then checks the home page and the database and renderer health routes. Release verification describes what to do when either job fails.
The published image names are:
ghcr.io/beastyrabbit/beastypage-frontendghcr.io/beastyrabbit/beastypage-rendererghcr.io/beastyrabbit/beastypage-image-processingghcr.io/beastyrabbit/beastypage-mediaghcr.io/beastyrabbit/beastypage-discord-bot
Frontend, renderer, and Discord images from one release carry the same generated cat-catalog hash. Their image builds fail when the supplied hash does not match the bundled contract, and runtime consumers fail closed when a renderer reports a different hash.
A release that changes this hash must therefore use a coordinated big-bang
cutover, not an ordinary mixed-version rolling deployment: build every consumer
from one tag, verify the new frontend and renderer stacks report the same hash,
then switch traffic to both together. Keep the previous stack available for
rollback until external smoke tests pass. Never suppress a catalog mismatch to
make a rollout green; that can return a valid response containing the wrong
pixels. The one-time v7.3.1 to v7.4.0 transition is compatible because the
legacy renderer does not advertise a catalog hash.
The frontend image connects to the PostgreSQL-backed API configured by its runtime environment:
docker run --rm \
-v /absolute/path/to/ca.crt:/etc/postgres-ca/ca.crt:ro \
-e NODE_ENV=production \
-e DATABASE_MIGRATION_URL='postgresql://owner:owner_password@db.example.com:5432/beastypage?sslmode=verify-full&sslrootcert=/etc/postgres-ca/ca.crt' \
-e DATABASE_APP_ROLE=beastypage_app \
ghcr.io/beastyrabbit/beastypage-frontend:8.1.0 \
node scripts/db/migrate.mjs
docker run --rm -p 3000:3000 \
-v /absolute/path/to/ca.crt:/etc/postgres-ca/ca.crt:ro \
-e NODE_ENV=production \
-e DATABASE_URL='postgresql://app:app_password@db.example.com:5432/beastypage?sslmode=verify-full&sslrootcert=/etc/postgres-ca/ca.crt' \
-e NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_replace_me \
-e CLERK_SECRET_KEY=sk_live_replace_me \
ghcr.io/beastyrabbit/beastypage-frontend:8.1.0The first command applies the schema with the owner URL before the app starts;
the second uses the app URL for serving requests. For a throwaway local setup,
DATABASE_ALLOW_INSECURE=1 is the documented override for a non-verified
connection.
For Kubernetes/Talos deployments, configure the PostgreSQL runtime values via the frontend runtime Secret and the CNPG cluster Secret:
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: frontend
image: ghcr.io/beastyrabbit/beastypage-frontend:8.1.0
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: beastypage-frontend-runtime-config
key: DATABASE_URLCat sprites originate from ClanGen, licensed under CC BY-NC 4.0.
- Original creator: just-some-cat.tumblr.com
- Fan-edit creator: SableSteel and others
Built with components from LifeGen, a ClanGen mod.
Additional tooling from Pixel Cat Maker.
- Code: MPL-2.0 (Mozilla Public License 2.0)
- Sprites/Art: CC BY-NC 4.0 (NonCommercial)
See LICENSE.md for full details.
Contributions are welcome! Please read CONTRIBUTING.md for guidelines.