ultratears
Unofficial native Windows port of Tears of the Kingdom 1.4.3 using static recompilation. Builds locally from the player's own extracted game dump; no game files included.
Amah는 커플과 친구를 위한 프라이빗 게임 방으로, 휘트·루도·퀴즈·연애 메모 등 실시간으로 휴대폰에서 즐깁니다
Amah is a private game room for couples, and for your best friends too. Whot, Ludo, trivia, love notes and a strictly opt-in after-dark corner for two, played live from your own phones.
게임TypeScript · PLpgSQLGitHub @stack-Tech-1온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 README에서 읽을 수 있습니다. 저장소 README ↗
README 전체를 불러오는 중입니다.
README 전문 보기 ↗A private game room for two. Partners pair up in a shared room and play 27 games and activities live from their own phones: head-to-head games, co-op puzzles, personal and romantic keepsakes, and an opt-in 18+ section.
Built with Next.js 15 (App Router), TypeScript, Tailwind CSS v4 and Supabase (Postgres, Auth,
Realtime, Storage). The product name lives in one place: APP_NAME in src/lib/config.ts.
npm install
cp .env.example .env.local # then fill it in (see below)
npm run db:migrate # applies supabase/migrations/*.sql
npm run dev # http://localhost:3000
Requires Node 22+.
If the Supabase keys are missing, the app shows a "not wired up yet" screen instead of crashing.
Create a project at supabase.com. Note its region.
API keys: Project Settings → API keys. Copy the project URL, the publishable (anon)
key, and a secret (service role) key into .env.local.
Database connection string (only used by npm run db:migrate):
Project Settings → Database → Connection string → Session pooler.
Use the pooler, not the "Direct connection": the direct host is IPv6-only and unreachable
from many networks. It looks like:
postgresql://postgres.<project-ref>:<password>@aws-0-<region>.pooler.supabase.com:5432/postgres
Run the migrations: npm run db:migrate. This creates every table, row-level-security
policy, function, the private memories storage bucket, and the Realtime publication.
Auth providers: Authentication → Sign In / Providers.
Redirect URLs: Authentication → URL Configuration.
https://amah.example.http://localhost:3000/auth/callback, http://localhost:3000/auth/confirm,
https://<your-domain>/auth/callback and https://<your-domain>/auth/confirm.Magic-link email (recommended): Authentication → Emails → Magic link. Replace the link in the template with:
<a href="{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email&next=/room">Sign in to Amah</a>
The default template works when the link is opened in the same browser that requested it. The token-hash version also works when someone requests the link on their laptop and opens it on their phone, which is common.
Email sending: Supabase's built-in sender is heavily rate-limited and meant for testing, and templates can only be edited once custom SMTP is set up (). A Gmail address with an app password works to start; use a provider with your own domain (e.g. Resend, Postmark) before launch.
No other dashboard setup is needed: tables, policies, storage and realtime all come from the migrations.
| Variable | Where | Purpose |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL | browser + server | Project URL. |
NEXT_PUBLIC_SUPABASE_ANON_KEY | browser + server | Publishable key. Safe to expose; every table is protected by RLS. |
SUPABASE_SERVICE_ROLE_KEY | server only | Secret key. Used for authoritative game state, account deletion and test setup. Never prefix it with NEXT_PUBLIC_. |
NEXT_PUBLIC_SITE_URL | browser + server | Canonical URL for invite links, Open Graph tags and the sitemap. http://localhost:3000 locally. |
SUPABASE_DB_URL | your machine / CI | Postgres session-pooler connection string, used only by npm run db:migrate. Not needed by the deployed app. |
NEXT_PUBLIC_ANALYTICS_PROVIDER | optional | Empty (off), console (logs events) or plausible. |
NEXT_PUBLIC_ANALYTICS_DOMAIN | optional | Your Plausible site domain, when the provider is plausible. |
AMAH_SANDBOX | optional | 1 enables /dev/play/<game> in a production build (used by tests). Leave unset in production. |
SMTP_USER, SMTP_PASS | optional, server only | Turn on announcement emails. With Gmail: the address and a 16-character App Password. |
SMTP_HOST, SMTP_PORT, EMAIL_FROM | optional | Another SMTP provider (defaults smtp.gmail.com, 465) and the From line. |
EMAIL_LINK_SECRET | optional, server only | Signs unsubscribe links. Defaults to the service role key; set it if you ever rotate that key and want old links to keep working. |
NEXT_PUBLIC_SENTRY_DSN | optional | Use a different Sentry project (the default is built in, src/lib/monitoring/config.ts). Error tracking runs on the live site only; NEXT_PUBLIC_SENTRY_ENABLED=1 forces it on elsewhere. |
.env.local is git-ignored. Never put real keys in .env.example: it is committed.
All schema lives in supabase/migrations/, applied in filename order:
| File | What it adds |
|---|---|
0001_foundation.sql | Profiles, rooms, membership, invite/pairing functions, moments timeline, rate limiting, Realtime channel authorisation. |
0002_game_engine.sql | Game sessions, hidden server-side state, per-player views, move log, atomic move commits, scoreboard. |
0003_personal.sql | "Complete" outcome; Daily Question with answers sealed until both have answered. |
0004_romantic.sql | Love notes (sealable), Memory Lane (private photo bucket), bucket list, trigger-based rate limits. |
0005_after_dark.sql | 18+ consent model, per-person PIN, custom cards, Fantasy Match (mutual matches only). |
0006_discreet_moments.sql | Keeps After Dark sessions off the shared timeline. |
0007_spicy_sessions_readable.sql | Lets an in-progress After Dark card finish after an opt-out. |
0008_chat.sql | Room chat: chat_messages, chat_reads (unread and "Seen") and mark_chat_read. |
0009_chat_media.sql | Voice notes and stickers: message kinds, stickers, the public stickers bucket (starter pack) and the private chat-media bucket. |
0010_admin.sql | Admin: app_admins, is_admin(), app_config (game switches), announcements, and the service-role-only admin_stats, admin_find_user, admin_remove_from_room. |
0011_content_email.sql | Editable content (content_items, content_hidden), announcement email opt-out and send log, admin_email_recipients, admin uploads to the starter-sticker bucket. |
0012_safety.sql | Consent records (18+, Terms version), reports with message snapshots and file_report(), blocks and block_partner(), and join_room refusing blocked pairs. |
0013_feedback.sql | Feedback threads (, , private screenshot bucket), suggested cards (), and the service-role-only . |
npm run db:migrate records what it has applied in public._twain_migrations (the original project name; kept so existing databases stay in sync) and only runs new
files, each in its own transaction. To change the schema, add a new numbered file; never edit
one that has already been applied.
You can also paste the files into the Supabase SQL editor in order, or use the Supabase CLI
(supabase db push).
npm run dev # Next dev server on :3000
npm run check # typecheck + lint + unit tests
Game sandbox: http://localhost:3000/dev/play/<game-id> (development only) renders a game
with both seats side by side, running the real game rules locally with no backend. It is the
fastest way to build or debug a game. Game ids are listed in src/games/catalog.ts.
Utility scripts
node scripts/build-words.mjs regenerates the Word Duel dictionary from the MIT-licensed
word-list package.node scripts/make-grain.mjs regenerates the paper-texture tiles.Unit tests (npm test, Vitest): game rules, edge cases, win conditions and the privacy of every
game's per-player view. They run in a couple of seconds.
End-to-end tests (npm run e2e, Playwright) run against a production build and a separate
test Supabase project, never the live one. They create throwaway users (amah-e2e-…@example.com),
switch games off, post announcements and so on, so e2e/helpers.ts refuses to start if the test
keys point at the live project. Setup, once:
amah-test..env.test.local (same names as .env.local: NEXT_PUBLIC_SUPABASE_URL,
NEXT_PUBLIC_SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, SUPABASE_DB_URL, plus
NEXT_PUBLIC_SITE_URL=http://localhost:3100 and AMAH_SANDBOX=1).npm run db:migrate:test to give it the schema. Run it again whenever a migration is added.Each run starts by deleting test accounts an interrupted earlier run left behind. Coverage:
First run: npx playwright install chromium.
Handy flags:
npm run e2e:build && npm run e2e:serve # a test-project build on :3100 (NEXT_PUBLIC_* are baked in at build time)
E2E_REUSE=1 npm run e2e # reuse that server
E2E_NO_SERVER=1 npx playwright test e2e/daily-privacy.spec.ts # database-only specs
AMAH_SANDBOX=1 npx next start -p 3100 # lets the per-board accessibility audit run
NEXT_PUBLIC_SITE_URL to the production URL. Don't set SUPABASE_DB_URL or
AMAH_SANDBOX there.…/auth/callback and …/auth/confirm URLs to Supabase's
redirect list (see Supabase setup).npm run db:migrate) before deploying any change that
needs a new one.Keep the functions next to the database. Every move makes several sequential database calls,
so the distance between the server functions and Supabase is paid several times per move.
vercel.json pins functions to dub1 (Dublin) to sit beside a Supabase project in eu-west-1
(Ireland). If your project lives elsewhere, change regions to the closest Vercel region (for
example iad1 for us-east-1, sfo1 for us-west-1, sin1 for ap-southeast-1).
netlify.toml is included. Netlify's Next.js runtime is detected automatically. Add the same
environment variables, deploy, and add the redirect URLs to Supabase. Middleware and server
actions run on Netlify Functions; nothing in the app is Vercel-specific. Set the Functions region
(Site configuration → Functions) to the one closest to your Supabase project.
/sitemap.xml, /robots.txt and /manifest.webmanifest.src/
app/ routes: (marketing) landing + legal, (auth) sign-in, (app) the product
games/
catalog.ts every game's name, category, tagline (the library is built from this)
logic.ts server-safe registry: id → pure game logic
registry.tsx client registry: id → board component
_engine/ GameLogic interface, seeded RNG, move runner
<game>/logic.ts rules (pure, deterministic, unit-tested)
<game>/Board.tsx UI
content/ every deck, question bank and word list (edit freely)
components/ room shell, game host, romantic and After Dark features
design/ design system: tokens are in app/globals.css
lib/ Supabase clients, server actions, realtime provider
supabase/migrations/ the whole database
e2e/ Playwright tests
GameLogic (src/games/_engine/types.ts): initialState,
validateMove, applyMove, isOver and viewFor. These are pure and deterministic, use a
seeded RNG, and are shared by the server and the tests.game_secrets, which clients cannot read at all. After each move the
server stores each player's viewFor(...) projection in game_views, and RLS lets you read
only your own. Hidden hands, answers and targets never reach the other phone.POST /api/sessions/:id/move). The server validates the move with Zod
and the game's rules, then commits it atomically with a version check. Simultaneous moves
retry, so neither is lost.Daily Question, Love Notes, Memory Lane, Date Night, the bucket list and Fantasy Match are
ongoing room features rather than turn-based sessions. They use their own tables, server
actions and RLS (see src/components/romantic, src/components/afterdark and src/lib/*).
Push notifications (src/lib/push). Web push with a VAPID key pair kept in app_secrets.
Create it once per project with npm run push:keys (-- --env .env.test.local for the test
project); re-running keeps existing keys. People turn notifications on per device in Settings,
where they also choose the kinds: game invites, your move, messages, Daily Question, nudges and
love notes. The open app sends a heartbeat to user_presence, and anyone seen in the last 70
seconds is skipped, so nobody is pinged about what they're already looking at. On iPhone,
notifications need Amah added to the Home Screen first.
Us (src/lib/progress). The couple streak counts days in a row where both did something.
Levels come from XP that weights shared effort over chatter. There are 17 badges with progress
bars, and a weekly recap card on Mondays covering the week before. The Together cards
(src/lib/together) cover days together and the anniversary, countdowns, a daily mood check-in,
and 30-day challenges (src/content/challenges.ts).
Chat (src/components/chat/Chat.tsx) sits in the room header, so it's one tap away on every
room page and mid-game. The browser writes messages straight to Supabase under RLS, then
broadcasts the saved row on the room channel for instant delivery. Postgres Changes and refetches
on reconnect are the backstop, and messages are deduped by id. In After Dark, a message preview
says "Sent you a message" and never quotes the text.
Voice notes record in the browser (MediaRecorder, up to 3 minutes) and upload to the private
chat-media bucket at <room>/voice/. Stickers come from two places: the starter pack every
couple gets, and each couple's own stickers made from their photos (<room>/stickers/, private).
To fill the starter pack from a folder (WhatsApp's sticker folder works as is):
npm run stickers:import -- "C:\Users\you\Downloads\WhatsApp Stickers"
Only use stickers you have the right to share with every user of the app.
/admin is for the people running Amah. Grant access to an existing account (sign in to the app
once with it first):
npm run admin:grant -- [email protected] # add
npm run admin:grant -- [email protected] --revoke # remove
SMTP_USER/SMTP_PASS). Each email
has a signed unsubscribe link and one-click unsubscribe headers, and people can switch emails
off or on in their room settings.npm run stickers:import).All admin reads go through SECURITY DEFINER functions that only the service role can execute,
and every admin action re-checks is_admin() on the server.
GAMES in src/games/catalog.ts (id, name, category, minutes, tagline).src/games/<id>/logic.ts exporting a GameLogic and write logic.test.ts next to it.src/games/<id>/Board.tsx. It receives GameProps: view, me, sendMove,
clock, broadcast and so on.src/games/logic.ts and one in src/games/registry.tsx.src/games/GameArt.tsx./dev/play/<id>), then run npm run check.Registry tests then check that the game is in the catalogue, that it never leaks its RNG seed to clients, and that the library has no dead entries.
Everything players read lives in src/content/ as plain TypeScript data:
| File | Used by |
|---|---|
know-me-bank.ts | Know Me Trivia question bank |
words/answers5.ts | Word Duel answers (each must also be a valid guess; a test enforces this) |
draw-words.ts | Draw & Guess prompts by difficulty |
split-clues.ts | Split Clues puzzles (both halves are needed; tests check structure) |
personal.ts | How Well, This or That, Story Chain openers, Deep Questions, Daily Questions |
date-ideas.ts | Date Night Generator |
after-dark.ts | Truth/Dare, Hot Seat, Never Have I Ever, Spicy Dice faces, Fantasy items |
All content is original. Players can also add their own After Dark cards, which stay private to their room.
SECURITY DEFINER functions.After Dark (18+), built to the owner's decisions:
spicy_unlocked(room) gates every spicy table and session.src/app/(marketing)/, operator details in src/lib/legal.ts). Have a lawyer
review them before you charge money. When they change materially, bump TERMS_VERSION and
everyone is asked to accept again.src/lib/monitoring/config.ts.
Reports are scrubbed of emails, IPs, tokens and typed text before they leave the device.public/fonts/NOTICE.txt.Branded emails: paste these into Authentication → Emails → Templates (Source tab):
| Template | Subject | File |
|---|---|---|
| Confirm sign up | Your Amah room is waiting | supabase/templates/confirm-signup.html |
| Magic link or OTP | Your Amah sign-in link | supabase/templates/magic-link.html |
| Change email address | Confirm your new email for Amah | supabase/templates/change-email.html |
They link to /auth/confirm with a token hash, so a link requested on one device works on any
other. Open the files in a browser to preview them. The logo is the static file
public/email/amah-mark.png, referenced by its full URL; if the production domain changes,
update that URL in all three templates.
feedbackfeedback_repliesfeedbacksuggestionsadmin_list_users0014_push.sql | Web push (push_subscriptions, push_prefs, app_secrets for the VAPID keys), user_presence heartbeat, nudges. |
0015_progress.sql | room_progress() (streaks, counts for levels and badges) and room_week() for the weekly recap. Members only. |
0016_together.sql | rooms.together_since, countdowns, moods, and 30-day challenges (room_challenges, challenge_checks). |