Modern CMS and item shop for Metin2 servers, built with Next.js, TypeScript and SSR.
This repository is the modern web platform for the Metin2 server environment in this lab.
The first product slice is intentionally small:
- login
- register
- account session handling
Later slices can expand into:
- account area
- rankings
- downloads
- item shop
- admin tooling
Current bootstrap decisions:
- Next.js App Router
- TypeScript
- pnpm
- Tailwind CSS
- Drizzle ORM + mysql2
- server-first architecture
The live game database is the source of truth for identity.
Read-only inspection on metin2server confirmed that the active account table is account.account, with legacy-compatible fields such as:
loginpasswordsocial_idemailstatuscashmileage
Important:
- the live schema does not expose
real_name - the live schema does not expose
coins - current passwords are stored in a legacy 41-character
PASSWORD()-style hash format
Because of that, the CMS must be designed around the real running DB contract, not around assumptions from old PHP CMS codebases.
The repository will keep two data domains clearly separated:
- Legacy game-owned data
account.accountremains the identity source of truth
- CMS-owned data
- web sessions
- auth audit logs
- token/recovery state
- later, item shop and operational tables
Required baseline:
- Node.js
22.22.2 - pnpm
10.33.0
FreeBSD note:
- Next.js 16 does not ship a native SWC binary for
freebsd/x64 - this repository installs
@next/swc-wasm-nodejsand links it duringpostinstall - local
devandbuildscripts use--webpackso the project works on FreeBSD
Environment note:
- unit tests and
pnpm builddo not require a live MariaDB connection anymore - the DB env is resolved lazily when auth/account code actually touches the database
- CI injects placeholder URLs so GitHub Actions does not fail on missing env config
- real login/register/recovery runtime still requires valid
DATABASE_URLandCMS_DATABASE_URL - live rankings runtime additionally requires
PLAYER_DATABASE_URL PLAYER_DATABASE_URLshould point to a read-only MariaDB user for theplayerschema, not to the auth or CMS write credentials- the authenticated
/accountcharacter panel also usesPLAYER_DATABASE_URLin read-only mode to load the live characters owned by the signed-in account - the public
/characters/[id]detail route also usesPLAYER_DATABASE_URLin read-only mode to load a single live character profile - private starter-pack delivery can be enabled with
STARTER_PACK_URL - protected starter-pack relay can additionally use
STARTER_PACK_USERNAME+STARTER_PACK_PASSWORD - the download surface can optionally expose the visible hash with
STARTER_PACK_SHA256 STARTER_PACK_URL,STARTER_PACK_USERNAMEandSTARTER_PACK_PASSWORDare runtime-only values and should stay out of committed source- when configured,
/downloadsexposes CMS-owned/downloads/clientand/downloads/client/checksumentrypoints so the page markup does not need to embed the backing distribution URL or auth details directly - recovery delivery is temporary for now:
- non-production defaults to
RECOVERY_DELIVERY_MODE=preview - production defaults to
RECOVERY_DELIVERY_MODE=file - file mode writes manual-delivery JSON payloads under
.runtime/recovery-outboxunlessRECOVERY_FILE_OUTBOX_DIRoverrides it RECOVERY_DELIVERY_MODE=previewis blocked in production on purpose
- non-production defaults to
- an integration job now boots a temporary MariaDB service in GitHub Actions and resets
account_test+metin2_cms_test - a local reset helper exists at
scripts/reset-test-databases-local.sh - integration helpers and reset scripts refuse to touch non-
*_testschemas
Before using login/register locally, provision the CMS-owned tables in the CMS database. A ready-to-apply SQL file lives at:
drizzle/0000_auth_tables.sql
Integration test assets:
- account test schema:
sql/test/account-test-schema.sql - CI/local admin reset script:
scripts/reset-test-databases.mjs - local FreeBSD reset script:
scripts/reset-test-databases-local.sh - integration tests:
tests/integration/auth/auth-flow.integration.test.ts
Main routes after this auth slice:
/login/register/recover/reset-password/account/characters/[id]
Main commands:
pnpm install
pnpm dev
pnpm lint
pnpm typecheck
pnpm test
pnpm db:test:reset:local
pnpm test:integration:local
pnpm buildThe deployed CMS runs continuously through a FreeBSD rc.d service:
- service name:
metin2_cms - port:
3000 - runtime entrypoint:
/opt/metin2/runtime/metin2-cms/start.sh
The runtime service serves the production Next.js build from this same working tree. That means a source change is not public until a fresh build is generated and the service is restarted. Host-specific domains, remotes and access details are intentionally kept out of this repository.
This repository includes a local push-and-deploy flow for the production working tree:
- push wrapper:
scripts/push-and-deploy.sh - git alias:
git push-deploy - deploy decision runner:
scripts/post-push-deploy.mjs - deploy script:
scripts/deploy-local.sh - deploy log:
/opt/metin2/logs/metin2-cms-deploy.log
Behavior:
- use
git push-deploy origin main(orscripts/push-and-deploy.sh origin main) for production pushes from this host - the wrapper pushes first and only starts the deploy if
git pushsucceeds - only pushes to
originthat updatemaintrigger a deploy - the remote URL is validated against the currently configured local
originremote instead of a hardcoded repository URL - dependency install runs automatically only when
package.json,pnpm-lock.yamlorpnpm-workspace.yamlchanged - deploys are serialized with a lock file so two pushes cannot overlap build/restart work
- every triggered deploy runs
pnpm build, restartsmetin2_cms, and waits for the local/loginhealthcheck on port3000 - the deploy script refuses to run if the repo is dirty or if
HEADdoes not match the pushed commit SHA - remote URLs are sanitized before they are written to the deploy log
Local repo configuration on the server uses:
git config alias.push-deploy '!sh scripts/push-and-deploy.sh'If a manual redeploy is needed on the server:
scripts/deploy-local.sh --sha="$(git rev-parse HEAD)"Add --install-deps when the dependency manifests changed.
Current phase:
- legacy-compatible login/register implemented
- login attempts now write
auth_audit_logentries and are rate-limited per login after repeated failures - password recovery slice implemented with CMS-owned tokens
- temporary recovery delivery modes in place (
previewfor dev,filefor production) - recovery requests now write
auth_audit_logentries and are rate-limited per login /accountnow shows active CMS sessions and can close the other sessions for the same account/accountnow supports revoking one specific non-current CMS session/accountnow shows the most recent auth activity for the account fromauth_audit_log- shadcn/ui is now installed as the CMS component primitive layer
- authenticated surfaces now use a darker modern dashboard/auth visual language instead of the original plain white milestone layout
/accountnow groups profile, game account, security center and recent activity with a stronger hierarchy- the public landing page and auth entry routes now share reusable CMS shells instead of standalone one-off wrappers
- the private web foundation now includes shared site navigation plus dedicated
/game,/downloads,/getting-startedand/rankingsroutes /downloadsand/getting-startednow document the private client delivery and onboarding flow without embedding host-specific URLs in the repository/downloadscan now expose a live starter-pack CTA throughSTARTER_PACK_URL, using/downloads/clientand/downloads/client/checksumas stable CMS-owned download and checksum paths- the landing, downloads and
/accountsurfaces now read like a real ready-to-brand server mock instead of internal project documentation /rankingsnow reads live character and guild ladder data from theplayerschema through a dedicated read-only database connection- ranking ordering is now documented in
docs/architecture/rankings.md - account character query and failure behavior are documented in
docs/architecture/account-characters.md - public character detail query and fallback behavior are documented in
docs/architecture/character-detail-route.md git push-deploy origin mainfrom the production working tree now pushes first and then rebuilds/restartsmetin2_cms/accountnow surfaces a security summary with active session count plus the latest successful sign-in, sign-in issue and latest account change/accountnow shows the live characters owned by the authenticated account via the read-onlyplayerschema connection/characters/[id]now shows a full public live character profile and is linked from rankings plus the authenticated account character cards/accountnow lets the authenticated user change the legacy-compatible password and revokes the other CMS sessions after a successful update/accountnow lets the authenticated user update the legacy account email and delete code from the protected area- the current CMS session now refreshes
last_seen_atwhen protected areas load - unit verification in place
- MariaDB-backed integration verification in place for register/login/recovery + CMS session persistence
Roadmap reference:
docs/plans/2026-04-18-web-product-roadmap.md
Next implementation order after this slice:
- item shop foundation and purchase audit model
- admin/editorial tooling