Credentials & Environment Variables
Status: Reference — Current
Date: 2026-05-06 · Last reviewed: 2026-11-13
Single source of truth for every credential TwoMore uses. See docs/best-practices/debugging-workflow.md for the Sentry pipeline mechanics. See docs/guides/api-supabase.md for Supabase-specific details.
Quick reference table
| Variable | Visibility | Local .env | EAS env | Purpose | How to obtain |
|---|---|---|---|---|---|
EXPO_PUBLIC_SUPABASE_URL | plaintext | required | set on all envs | Supabase project endpoint | Supabase dashboard → Settings → API → Project URL |
EXPO_PUBLIC_SUPABASE_ANON_KEY | plaintext | required | set on all envs | Supabase anon JWT (safe in client) | Supabase dashboard → Settings → API → anon key |
EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY | plaintext | optional | set on all envs | Newer sb_publishable_* format key | Supabase dashboard → Settings → API → publishable key |
EXPO_PUBLIC_USE_MOCK | plaintext | optional | not set (defaults false) | Toggle mock adapters for dev | Hardcode false; set true only during local unit dev |
EXPO_PUBLIC_KAKAO_APP_KEY | plaintext | required | set on all envs | Kakao Login native SDK init; fallback Kakao Share native plugin key | developers.kakao.com → 내 애플리케이션 → 앱 키 → 네이티브 앱 키 |
TWOMORE_KAKAO_NATIVE_APP_KEY | plaintext | optional | preview / production build env | Preferred R1 Kakao Share native plugin key. Uses the same Kakao Native App Key as EXPO_PUBLIC_KAKAO_APP_KEY; set this for clarity, or rely on the existing EXPO_PUBLIC_ fallback | Same Kakao Native App Key as above |
TWOMORE_KAKAO_TEMPLATE_* | plaintext | optional | preview / production build env | R1 KakaoTalk Share custom-template IDs for session, club, match result, tier promotion, stat card, and leaderboard shares | Kakao Developers → 메시지 템플릿 → approved custom-template ID |
TWOMORE_APPLE_TEAM_ID | plaintext | optional | web deploy env | iOS Universal Links AASA appID value for twomore.app | Apple Developer → Membership details → Team ID |
TWOMORE_ANDROID_SHA256_CERT_FINGERPRINTS | plaintext | optional | web deploy env | Android App Links assetlinks.json certificate fingerprint list for com.ivorybridge.twomore | EAS/Play signing certificate SHA-256 fingerprint; comma-separate preview/production fingerprints if both should verify |
EXPO_PUBLIC_NAVER_MAPS_CLIENT_ID | plaintext | required | set on all envs | Naver Maps client id: the venue map picker (packages/ui/src/venue-location-picker.tsx, packages/features/sessions/src/venue-contributions/location-editor.tsx) reads it in the app bundle; without it the picker has no map | See api-naver-search |
EXPO_PUBLIC_NAVER_API_HUB_CLIENT_ID, EXPO_PUBLIC_NAVER_API_HUB_CLIENT_SECRET | plaintext | scripts only | local env only; must not be set in the build environment | Naver API hub id and secret for the venue scripts (scripts/enrich-venues.mjs, triangulate-coords.mjs, repair-coords.mjs and three more). No app-bundle code reads either; a _SECRET name must never be given to a build, because a public-prefixed variable is inlined into the bundle | See api-naver-search |
DATA_GO_KR_SERVICE_KEY | secret | optional (server-only) | Supabase Edge Function Secrets only | data.go.kr service key — ACCOUNT-WIDE: authorizes every API approved on the account (한국천문연구원 특일정보, 소상공인 상가정보, 등록체육시설업, …), not just holidays. Read server-side only by supabase/functions/fetch-holidays; must never be EXPO_PUBLIC_* or ship in the client bundle. | data.go.kr → 한국천문연구원 특일정보 API → 활용신청, then npx supabase secrets set DATA_GO_KR_SERVICE_KEY=... |
EXPO_PUBLIC_DEV_EMAIL | plaintext | optional, untracked env only | NOT set (no build profile holds it) | Developer one-tap button: shown only when BOTH this and the password are set in the build's own environment | A developer's own untracked .env; never in eas.json |
EXPO_PUBLIC_DEV_PASSWORD | plaintext | optional, untracked env only | NOT set (no build profile holds it) | The pair of the line above; also read by the local scripts (yarn debug:logs, yarn qa:telemetry, yarn qa:health) | A developer's own untracked .env; never in eas.json |
EXPO_PUBLIC_SENTRY_DSN | sensitive | optional | set on preview + production | Runtime Sentry SDK init — inlined into JS bundle at build time | sentry.io → Settings → Projects → twomore-v2 → Client Keys (DSN) |
SENTRY_AUTH_TOKEN | secret | not needed locally | set on preview + production | Build-time sentry-cli sourcemap upload to Sentry — never inlined into binary | sentry.io → Settings → Auth Tokens → Create new org-level token. Required scopes: project:releases, project:write, org:read |
SUPABASE_ACCESS_TOKEN | secret | not needed locally | GitHub Actions repo secret only | CI: opt-in Supabase type-drift gate (regenerates generated.types.ts from live schema, fails if drift) | Supabase Dashboard → Settings → Access Tokens → "Generate new token" → paste into GitHub repo settings → Secrets and variables → Actions |
TWOMORE_SECRET_KEY | secret | n/a (server-only) | Supabase Edge Function Secrets only | Post-Path-B rotation: edge functions read this sb_secret_* value instead of the legacy SUPABASE_SERVICE_ROLE_KEY reserved name. Rotates per-key without affecting user sessions. | Supabase Dashboard → Settings → API Keys → "New API key" → Secret → set via npx supabase secrets set TWOMORE_SECRET_KEY=sb_secret_... |
EAS env vs local .env
How EAS environment variables work
EAS has three variable visibility levels:
| Level | Dashboard visible? | Build machine readable? | Can be EXPO_PUBLIC_*? |
|---|---|---|---|
plaintext | Yes (shown in UI) | Yes | Yes |
sensitive | Masked in UI | Yes (readable on build machine; may appear in compiled bundle) | Yes — use for EXPO_PUBLIC_* values that get inlined |
secret | Never | Yes (only on build machine) | No — secrets cannot be EXPO_PUBLIC_* because those must be readable at JS-bundle compile time |
Build-time vs runtime distinction
EXPO_PUBLIC_* variables are inlined into the JS bundle at Metro compile time. This means:
- Setting an
EXPO_PUBLIC_*variable after a build has no effect — you must rebuild. - They end up in the binary (and can be extracted with tooling). Use
sensitivevisibility for values that are technically embeddable but should not be trivially visible in the EAS dashboard. EXPO_PUBLIC_SENTRY_DSNis the key example: it needs to be in the bundle for the SDK to initialize, but you still want it masked in dashboards — hencesensitive.
Non-prefixed variables like SENTRY_AUTH_TOKEN are available only to the build environment (gradle, metro config, EAS build scripts). They are never inlined into the runtime binary.
Precedence rule
If a variable is set in both eas.json's env block AND via eas env:create, the eas.json value wins. Since 2026-11-13 no build profile in eas.json holds an env key (all three env objects are empty), so every build variable comes from EAS: read the NAMES with eas env:list preview before a build. The developer sign-in pair is not set on EAS either (see below).
Signing in to test (as of 2026-11-13)
A tester types. The sign-in page has an e-mail field and a password field. There is no auto-filled password in any build.
| Who | How they sign in | Where the password lives |
|---|---|---|
| The developer account (owner of the seeded clubs) | Type the e-mail and the password on the sign-in page | Two untracked environment files on the owner's machine. It was rotated on the hosted project on 2026-11-13; the value that appears in the git history is dead. This document never holds it. |
| The one-tap "Dev" button | Shown only on a build whose own environment sets BOTH EXPO_PUBLIC_DEV_EMAIL and EXPO_PUBLIC_DEV_PASSWORD (a local development build) | The developer's untracked .env. Since 2026-11-15 the build service's PREVIEW environment also holds both (the owner's instruction: enter through the developer account while the app is being examined), so a preview build draws the button. Production holds neither. |
Seeded people (qaready{n}@test.twomore.dev, player{n}, refund{n} and the other seeded prefixes) | Type the e-mail and the seed password | The edge function secret SEED_ACCOUNT_PASSWORD, set by the owner on the hosted project (and in the local functions env file for a local seed). The seed refuses to create accounts when it is unset. Nothing in the repository holds the value. |
| The Maestro flows | -e QA_EMAIL=... -e QA_PASSWORD=... from the shell; flows 05 and 07 read QA_EMAIL_05/QA_PASSWORD_05 and QA_EMAIL_07/QA_PASSWORD_07 | The runner takes them from the shell environment and masks them when it prints a command. A flow file never holds one. |
Two phones, two people. Phone one signs in as the developer account; phone two signs in as the seeded person named in the List 53 item. Where an item needs the developer account in a second role, change the role by running the scenario that sets it (club_growth flips the caller between owner, admin, member and outsider) instead of sharing one login.
What a seeded person is for. Each qaready{n} account is named for the state it shows (for example "QA 가입 제안 기다림" holds a waiting membership offer). The list with the e-mail number of each is in List 53, "Accounts".
Rotating the developer password again. Change it in the Supabase dashboard (Authentication, Users) for the hosted project, then edit the two untracked env files. Nothing else in the repository changes, because nothing else holds it. A hosted credential change is the owner's.
The developer functions' switch. The telemetry, server-health and seed tools need the developer account AND app_config.allow_dev_rpcs on the project. Set it on for the test window and off at launch (release evidence S29). The hosted seed also needs SEED_ALLOWED_UIDS (the developer account's id) and, while the project is called production, the owner's unlock (SEED_PROD_UNLOCK); see qa-automation.md.
Optional: signed updates. The key pair is a credential (the private key). Its steps are in pre-build-checklist.md, "Signed updates".
Adding a new credential
Add the variable to
.env.examplewith a comment header following the existing style (service name + obtain instructions).Set the variable on EAS via:
basheas env:create --name MY_NEW_VAR --value "the-value" --visibility plaintext # or --visibility sensitive / --visibility secretDocument the variable in the quick reference table above.
Update the consumer code to read
process.env.MY_NEW_VAR(orConstants.expoConfig.extra.myNewVarif threaded throughapp.config.ts).If it is a
EXPO_PUBLIC_*variable, rebuild — OTA alone will not pick up the new value.
Rotation procedure
Supabase anon key
Supabase dashboard → Settings → API → Rotate anon key.
Update
EXPO_PUBLIC_SUPABASE_ANON_KEYon EAS:basheas env:create --name EXPO_PUBLIC_SUPABASE_ANON_KEY --value "new-key" --visibility plaintext --forceRebuild the app — the old key is inlined in existing binaries.
Supabase publishable key
Same procedure as anon key above but for EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY.
Sentry DSN
The DSN is not a secret; it is safe to rotate if a project is compromised.
sentry.io → Settings → Projects → twomore-v2 → Client Keys → Revoke the old DSN → Create new.
Update on EAS:
basheas env:create --name EXPO_PUBLIC_SENTRY_DSN --value "https://new-key@ingest.sentry.io/xxx" --visibility sensitive --forceRebuild — DSN is inlined at build time.
Sentry auth token
This is the most sensitive credential because it can upload sourcemaps. Rotate immediately if compromised.
sentry.io → Settings → Auth Tokens → Revoke the old token.
Create a new org-level token with scopes
project:releases,project:write,org:read.Update on EAS:
basheas env:create --name SENTRY_AUTH_TOKEN --value "sntrys_new_token" --visibility secret --forceNo rebuild required — this variable is not inlined into the binary.
Kakao app key
developers.kakao.com → 내 애플리케이션 → 앱 설정 → 앱 키 (네이티브 앱 키).
Update on EAS:
basheas env:create --name EXPO_PUBLIC_KAKAO_APP_KEY --value "new-key" --visibility plaintext --force eas env:create --name TWOMORE_KAKAO_NATIVE_APP_KEY --value "new-key" --visibility plaintext --forceTWOMORE_KAKAO_NATIVE_APP_KEYis the preferred R1 Kakao Share build-time variable;EXPO_PUBLIC_KAKAO_APP_KEYremains a backward-compatible fallback. Do not set the two names to different Kakao apps.Rebuild.
KakaoTalk Share template IDs
developers.kakao.com → 내 애플리케이션 → 메시지 템플릿.
Create or update the six R1 custom templates represented in
packages/app/src/config/kakao-share-templates.json.Update the approved template IDs on EAS:
basheas env:create --name TWOMORE_KAKAO_TEMPLATE_SESSION_INVITE --value "template-id" --visibility plaintext --force eas env:create --name TWOMORE_KAKAO_TEMPLATE_CLUB_INVITE --value "template-id" --visibility plaintext --force eas env:create --name TWOMORE_KAKAO_TEMPLATE_MATCH_RESULT --value "template-id" --visibility plaintext --force eas env:create --name TWOMORE_KAKAO_TEMPLATE_TIER_PROMOTION --value "template-id" --visibility plaintext --force eas env:create --name TWOMORE_KAKAO_TEMPLATE_STAT_CARD --value "template-id" --visibility plaintext --force eas env:create --name TWOMORE_KAKAO_TEMPLATE_LEADERBOARD --value "template-id" --visibility plaintext --forceRebuild; these IDs are exposed to the app through Expo
extraat build time and are verified byyarn check:r1-kakao-share:credentials.
Naver client credentials
- Reissue the key where it was issued (see api-naver-search).
- For the map: update
EXPO_PUBLIC_NAVER_MAPS_CLIENT_IDon EAS (sameeas env:create --forcepattern) and rebuild. The API hub id and secret are read by local scripts only: update them in your own untracked env, never on EAS.
Cross-references
- Sentry pipeline mechanics (how
captureCriticalErrorflows through console → Supabase → Sentry, how to query errors, MMKV crash log layer):docs/best-practices/debugging-workflow.md - Supabase specifics (service-role key, Storage, Realtime, migrations):
docs/guides/api-supabase.md - Authentication flow (Kakao/Google/Apple OAuth via Supabase PKCE):
docs/guides/authentication.md - Build & OTA deployment (EAS builds, OTA channels, runtime versioning):
docs/guides/builds-and-deployment.md - External services index:
docs/guides/external-apis.md