New
Apps 1,360Makers 1,358This week 1,360
Promote your app−30%
1views in
the last 30 days

Unfit For Print

A Cards Against Humanity style party game played online.

Built with Claude Code

by ppo-gg · Unclaimed · added Oct 29, 2026
no ratings0 comments
Unfit for Print logo

UNFIT FOR PRINT

A party game for hideous people.

Play now at unfit.cards · How to play · Changelog · Report a bug

Unfit for Print main menu: a deck of sample cards beside tiles for New Game, Join Game, Games, Labs, How to Play and Settings

Unfit for Print is a Cards Against Humanity-style game for the browser. Start a lobby, share the four-letter code, and take turns being terrible. One player judges each round, everyone else plays their worst white card, and the judge picks a winner. First to the target score wins.

Made by MYND at PPO.GG.

Not affiliated with Cards Against Humanity — we just love their game.

Screenshots

Lobby table with the host and five bots seated in a ring, lobby chat on the left, and the lobby code, player list and game settings on the right A round in progress: the black card on the left, submitted cards stacked face-down in the middle, and the player's hand fanned along the bottom
The lobby: fill empty seats with bots while you wait for friends Mid-round: pick a card from your hand before the judge reveals

Features

At the table

  • Real-time multiplayer. Every play, reveal and pick syncs instantly across the table.
  • Rotating judge. Everyone takes a turn choosing the funniest (or most horrible) answer, and the judge can skip one dud prompt per round.
  • Bots. The host can fill empty seats with up to five bots, so a game of three still feels like a party.
  • Card packs. Choose which decks to play with each game, including the community-made Unfit Labs pack.
  • Read aloud. The judge can have cards read out to the whole table with text-to-speech.
  • Host controls. Set the target score and hand size, and skip a stalled player or judge so the round keeps moving.
  • Spectators. Watch a game in progress and deal yourself in when you're ready.

Lobbies and social

  • Public or private lobbies. Browse open games or share a code and invite link. Private lobbies can require a password.
  • Lobby chat for talking trash between rounds.
  • Discord Activity. Play right inside a Discord voice channel.
  • Discord login or guest. Jump in without an account, or sign in to keep your profile.
  • Avatar decorations to dress up your seat.

Unfit Labs

  • Write your own cards. Submit white or black cards, vote on other people's, and the best ones get adopted into the Unfit Labs pack.

Everywhere else

  • 9 interface languages: English, Spanish, French, German, Portuguese, Russian, Japanese, Korean and Chinese. Card text stays in English because the jokes don't survive translation.
  • Installable as a PWA on desktop and mobile.
  • Sound effects to keep the energy up.

Tech stack

Layer Tech
Framework Nuxt 4, Vue 3 Composition API
UI Nuxt UI v4, Tailwind CSS 4, GSAP
Real-time Yjs CRDTs on Cloudflare Durable Objects
Backend Nitro server routes, PostgreSQL with Drizzle ORM
Auth Discord OAuth2 and guest accounts (nuxt-auth-utils)
i18n @nuxtjs/i18n, 9 locales
Testing Vitest
Hosting Self-hosted Docker images, published to GHCR on release

How it works

State is split by how long it needs to live.

  • Live game state (hands, submissions, scores, whose turn it is) lives in a Yjs document. Every game action runs in the players' browsers and syncs through the sync worker in sync-worker/, a Cloudflare Worker with one Durable Object per lobby. The document is saved to Durable Object storage, so it survives restarts and deploys, and is deleted 10 minutes after the last player leaves.
  • Durable data (users, lobbies, cards and packs, Labs submissions, reports) lives in Postgres and is reached only through the Nitro API in server/api/.
flowchart LR
    B1[Player browser] <-- Yjs over WebSocket --> T[Sync worker]
    B2[Player browser] <-- Yjs over WebSocket --> T
    B1 -- REST --> W[Nuxt / Nitro API]
    B2 -- REST --> W
    W --> DB[(PostgreSQL)]

The game rules live in app/composables/useYjsGameEngine.ts. For the full architecture, conventions and gotchas, read AGENTS.md.

Running locally

You'll need Node 22, pnpm 10 (corepack enable gets you the pinned version), and Postgres 16. Docker is the easiest way to get Postgres.

1. Install and configure

pnpm install
cp .env.example .env

DATABASE_URL and NUXT_SESSION_PASSWORD are required. Generate the session password with openssl rand -base64 32. Everything else is optional: the Discord keys are only needed for Discord login and the Activity, and the TTS and R2 keys only for read-aloud voices and decoration uploads. Guest play works without them.

2. Start Postgres and apply migrations

docker run -d --name unfit-postgres -p 5432:5432 -e POSTGRES_USER=unfit -e POSTGRES_PASSWORD=unfit -e POSTGRES_DB=unfit postgres:16-alpine
pnpm db:migrate

That container matches the default DATABASE_URL in .env.example.

3. Run the sync server

.env.example points at the production sync worker (wss://sync.unfit.cards). For development, run your own so your test lobbies stay local:

cd sync-worker && pnpm install && cp .dev.vars.example .dev.vars && pnpm dev

Then set NUXT_PUBLIC_LOBBY_TELEPORTAL_URL=ws://localhost:1235 and NUXT_SYNC_ADMIN_TOKEN=dev-sync-admin-token in .env.

4. Start the app

pnpm dev

The app runs at http://localhost:3000.

5. Add some cards

A fresh database has no cards. To load some, sign in once (guest is fine), then make yourself an admin by setting is_admin = true on your row in the users table. pnpm db:studio is the quickest way to do that. Then open /admin/cards/upload and upload a card-pack JSON: an array of { name, white: [...], black: [{ text, pick }] } packs. The schema is in server/api/dev/seed.ts.

Tests

pnpm test

The server and database suites delete table data before every test, so they refuse to run unless TEST_DATABASE_URL is set and differs from DATABASE_URL. Point it at a throwaway database, never your real one:

docker run -d --name unfit-postgres-test -p 5433:5432 -e POSTGRES_USER=unfit -e POSTGRES_PASSWORD=unfit -e POSTGRES_DB=unfit postgres:16-alpine

Then apply migrations to it with DATABASE_URL=$TEST_DATABASE_URL pnpm db:migrate.

Contributing

Found a bug or have an idea? Open an issue. Pull requests are welcome.

  • Commits follow Conventional Commits, and commitlint enforces this in CI. The scopes in use are web, server and bot.
  • release-please builds releases and the changelog from those commits, so don't edit CHANGELOG.md or the version in package.json by hand.
  • All work targets main.
  • New user-facing strings go in i18n/locales/en.json at minimum.

Still in active development. Things might break, and things might not work as intended.

License

Licensed under CC BY-NC-SA 4.0. See LICENSE for details.

Description taken from the repository README.

Ratings

–
0 ratings
5 0
4 0
3 0
2 0
1 0

Rate this app

Sign in to rate

Comments 0

Sign in to comment

No comments yet.

Related apps

#party-game · #cards · #multiplayer