38 KiB
Architecture
Last source audit: 2026-05-12, based on git commit 8ee86a9.
System Overview
MiaoJing is a self-hostable AI multimodal creation platform built on:
- Next.js 16 App Router
- React 19
- TypeScript
- PostgreSQL via
pg - Local disk storage through
/api/local-storage/* - PM2 process management
- Optional upstream AI providers through SDK, custom API keys, and admin system API configs
The repository contains frontend pages, API route handlers, business services, database helpers, storage helpers, deployment scripts, and PM2 config in one app.
Runtime Shape
Browser
|
| HTTP
v
Next.js App Router
|
+-- src/app pages
+-- src/app/api route handlers
|
+-- src/components UI and workflow components
|
+-- src/lib business logic and stores
|
+-- src/storage database clients
|
+-- local disk storage via src/lib/local-storage.ts
|
+-- PostgreSQL via src/storage/database/local-db.ts
|
+-- Upstream AI providers via SDK/custom fetch
Production is controlled by ecosystem.config.cjs and the scripts under scripts/. Always verify PM2 cwd and runtime environment before editing a live server.
Directory Boundaries
| Directory | Responsibility |
|---|---|
src/app |
Next.js pages, layouts, and API route handlers. |
src/components |
Reusable UI and product workflow components. |
src/components/ui |
Radix/shadcn-style primitives. Keep generic. |
src/components/create |
Create center panels and shared generation UI. |
src/components/admin |
Admin console tab components. |
src/components/profile |
Profile page tabs and API key manager. |
src/lib |
Business logic, stores, model config, auth, storage, email, generation jobs, provider resolution. |
src/modules |
Thin module export and console page wrappers. |
src/storage |
Database client/schema compatibility. |
scripts |
Build, deploy, backup, restore, DB patch, admin upgrade runner. |
public |
Public static assets. |
assets |
Project-owned source assets. |
Frontend Architecture
The app is route-driven through src/app:
- Public shell:
src/app/layout.tsxandsrc/components/app-shell.tsx. - Navigation and global UI: navbar, footer, announcement popup, site config sync, visit tracker.
- Product workflows:
- Create center:
src/app/create/page.tsxplussrc/components/create/*. - Gallery:
src/app/gallery/page.tsx. - Profile:
src/app/profile/page.tsxplussrc/components/profile/*.
- Create center:
- Admin console:
src/app/console/*,src/modules/console/pages/*,src/components/admin/*.
Mobile adaptation is handled primarily through page-level structure classes plus src/app/globals.css. The create center uses .create-chat-layout, .create-chat-thread, and .create-chat-composer so phones behave like a modern AI chat client: the single mode switch is the sticky icon row, the page title and duplicate text mode strip are hidden, and text-to-image reads as a chronological conversation from oldest to newest so the latest work sits above the fixed composer. Text-to-image suppresses the empty result placeholder until the user submits a prompt, then renders the generating task as the newest prompt-plus-progress message. src/components/create/mobile-creation-composer.tsx is the fixed bottom input shell for text-to-image; it holds extra-compact labeled ratio/resolution/count controls and similar parameters, the optional style strip, the prompt input, and the right-side send button, and intentionally does not duplicate mode switching. Gallery masonry keeps at least two columns on phone widths. The admin console keeps the drawer navigation from console-dashboard-page.tsx and uses console-mobile-* shell rules to constrain cards while allowing dense admin tables to scroll horizontally instead of overflowing the viewport.
Client stores in src/lib/*-store.ts mediate API calls and local UI state. When fixing a UI persistence bug, inspect both the component and the matching store/API route.
Navigation performance is handled as part of the frontend architecture, not only by backend route timing. src/components/navbar.tsx defers its initial logged-in profile refresh so a fresh page load does not compete with the user's first navigation, but it should not eagerly prefetch every core route from the navbar because production web users can see that extra resource pressure as slower visible page switches. src/components/visit-tracker.tsx posts site statistics with keepalive after idle time because analytics should not block first paint. src/app/create/page.tsx keeps the primary creation workflow panels in the initial client bundle so switching between text/image/video/reverse-prompt modes does not wait on extra ssr:false chunks or show fallback flashes. The create panels must request lightweight scoped history through useCreationHistory({ mode, limit }); production users with large histories can otherwise trigger repeated multi-MB /api/creation-history responses that compete with navigation and image loads. src/lib/creation-history-store.ts owns short-lived in-flight request reuse and merges scoped responses into local history rather than replacing unrelated modes. src/app/profile/page.tsx keeps the parent account shell light and lets creation history, credit records, and orders mount their stores only inside their respective tab components. Keep scripts/test-navigation-performance-policy.mjs aligned with these constraints when changing the app shell, create center, profile page, or history store.
API Architecture
All APIs are route handlers in src/app/api.
Major groups:
auth/*: login, register, admin existence, provider API testing.profile/*: user profile and theme.user-api-keys: user-owned custom API credentials.model-config: public model/provider config.style-presets: public DB-backed image style presets ordered by usage count.generate/*: direct generation, reverse prompt, prompt suggestion.generation-jobs/*: queued generation job creation/status.creation-history: user works/history.gallery/*: public gallery and publishing.admin/*: console dashboard, users, providers, system APIs, orders, payment, upgrade, data import/export, email settings.redeem-codes/*andcredit-transactions: user credit redemption and credit record APIs.site-config,site-stats,announcements: public site content and counters.local-storage/*,download: file serving and download proxy.
Auth is not implicit. Each route must call the correct helper:
- User route:
getAuthenticatedUserIdorgetAuthenticatedUser. - Admin route:
requireAdminorrequireAdminUser. - Internal generation route:
isTrustedInternalGenerationRequest.
Generation Flow
Create panel
-> src/lib/generation-job-client.ts
-> POST /api/generation-jobs
-> system-default credit preflight counts selected job plus same-user queued/running jobs
-> generation_jobs row inserted
-> src/lib/generation-job-worker.ts
-> src/lib/generation-job-runner.ts
-> POST /api/generate/image or /api/generate/video
-> SDK or custom/system API upstream call
-> src/lib/local-storage.ts persists result
-> src/lib/generation-credit-service.ts deducts selected system API credits only after success
-> generation_jobs updated with result/error/progress
-> client polls GET /api/generation-jobs/[id]
-> create panels can recover queued/running jobs from GET /api/generation-jobs after refresh, auth change, or tab switch
(anonymous recovery list polling is skipped, and same-token/type list requests are briefly deduped client-side)
-> client-side pending job ids also query GET /api/generation-jobs/[id] so jobs that reached succeeded/failed/cancelled while the browser was closed still display their terminal state once before being cleared
-> history/gallery persistence via works APIs
Key source files:
src/components/create/text-to-image.tsxsrc/components/create/image-to-image.tsxsrc/components/create/text-to-video.tsxsrc/components/create/image-to-video.tsxsrc/lib/generation-job-client.tssrc/app/api/generation-jobs/route.tssrc/app/api/style-presets/route.tssrc/lib/style-preset-store.tssrc/lib/generation-job-worker.tssrc/lib/generation-job-runner.tssrc/app/api/generate/image/route.tssrc/app/api/generate/video/route.ts
Do not bypass the job flow unless intentionally implementing synchronous/internal-only behavior.
Provider Resolution
There are three provider sources:
- Built-in model config:
src/lib/model-config.ts. - User custom API keys:
src/app/api/user-api-keys/route.ts,src/lib/custom-api-store.ts. - Admin system APIs:
src/app/api/admin/system-apis/route.ts,src/lib/server-api-config.ts.
src/lib/server-api-config.ts resolves:
customApiKeyIdinto a user-owned decrypted API config.systemApiIdinto an active admin-managed decrypted API config after checking platform-default visibility and the requesting user's membership tier.systemApiIdpolling candidates for admin default models by matching media type plus admin display name (system_api_configs.name) across active/default system API rows and ordering them bypolling_modepluspolling_order/sort_order; each candidate still sends its own provider-specificmodel_nameupstream.- direct
apiKeypassthrough for legacy/custom callers.
Secrets must be encrypted at rest with src/lib/server-crypto.ts and never returned in API responses.
User-level intelligent API imports add a fourth data artifact tied to source 2: a per-key JSON Manifest in local storage. src/app/api/user-api-keys/smart-import/route.ts parses either a full { customProviders, profiles } bundle or one provider Manifest, creates a separate user_api_keys row for every profile/model, and writes user-api-manifests/<userId>/<keyId>.json. user_api_keys.manifest_path is the only runtime pointer. The imported row keeps a human-readable provider/supplier name for editing and derives the visible request URL from the Manifest profile/provider; incomplete configs without a resolvable relay API request URL are rejected. Optional profile.capabilities is stored in the Manifest and returned to the client so the selected model can constrain or hide image aspect ratio, resolution, image format, and quality choices. Manifest poll endpoints should put task IDs in query: { task_id: "{task_id}" } when the upstream documents a query string, so the executor sends a real query parameter instead of embedding ?task_id= into the pathname. Even for the same user, different request configuration files must remain separate because generation dispatch is selected-model based, not user based.
At generation time, src/lib/server-api-config.ts returns manifestPath for user custom keys and admin system API keys. src/app/api/generate/image/route.ts and src/app/api/generate/video/route.ts call src/lib/user-api-manifest-executor.ts first when that path exists. The executor handles JSON, multipart file fields, {task_id} polling, * JSON-path extraction, and media persistence handoff. For image Manifest results, the route persists returned result URLs through src/lib/media-storage.ts; external result URL downloads use src/lib/remote-fetch.ts with browser-like headers and limited retry so provider/CDN-side 403, 429, 5xx, or timeout failures are distinguished from upstream generation failures. If the provider returned a result but MiaoJing cannot download or save the image media, the API should report a platform download/save failure instead of a resolution mismatch. Imported Manifest rows still need the user or admin to edit and save an API Key before they can generate.
Manifest template rendering exposes input images in two forms: $inputImages.dataUrls keeps the raw uploaded data for multipart/file manifests, while $inputImages.urls is normalized for providers that require URL references. The executor converts data URL references to storage-backed public URLs before rendering JSON templates, using object-storage signed URLs when available or the app public base URL plus /api/local-storage/<key> otherwise.
Admin system intelligent API imports live in src/components/admin/api-management-tab.tsx and src/app/api/admin/system-apis/smart-import/route.ts. The 智能配置 API section is generic Manifest import only: each imported profile/model becomes one global system_api_configs row with its own manifest_path, backed by system-api-manifests/<systemApiId>.json, and the visible api_url is resolved from the Manifest profile/provider. Incomplete configs without a resolvable relay API request URL are rejected. Optional profile.capabilities can constrain or hide create-page image/video parameter choices for system models. Provider-specific built-in templates such as 元界 AI are not exposed in this smart import UI; 元界 definitions remain in src/lib/yuanjie-image-model-templates.ts and src/lib/yuanjie-video-model-templates.ts for the system-default-model management path, where admins configure each model row's Key, pricing/member visibility/polling, video_usage_modes, and enablement before it is available to users. 元界 price and billing metadata sync is also provider-specific and manual: /api/admin/system-apis/yuanjie-pricing uses src/lib/yuanjie-pricing-sync.ts to update only existing provider = '元界 AI' rows with derived billing mode and price notes, preserving API keys, Manifest paths, mozheAPI rows, and administrator-entered numeric prices. Admin Manifest files must remain separate from user-level files and must keep using the system pricing/credit deduction policy for the selected model. System API rows also own is_default, allowed_membership_tiers, polling_mode, and polling_order; /api/model-config returns only one active platform-default row per allowed media type plus admin display name so the create page shows a single default model label, and image generation expands the selected row back into all allowed supplier candidates with the same display name. The upstream model_name can differ between suppliers and is only used as that supplier's request model. Video model billing supports per-use count (fixed), per-second duration (duration_price_per_second), and token mode. Token billing prices shown in the admin console are credits per 1M tokens for both input and output; older storage/API field names containing 1k remain compatibility names and must not be shown to admins as per-K pricing. If a system image supplier fails because a stream request idles until Cloudflare 524, /api/generate/image retries that candidate once with stream:false; 502/503/504 gateway responses are retried once by the shared transport. If every supplier still fails or returns no usable result, the route returns the last actionable upstream error when available, otherwise the generic model-busy message. This polling fallback is only for admin default system models and must not be applied to user custom API keys.
After production migration, app runtime tables in public should be owned by the app DB user from LOCAL_DB_URL. Runtime compatibility helpers use ALTER TABLE ... ADD COLUMN IF NOT EXISTS and index creation; if restored tables remain owned by postgres, public routes such as /api/model-config, profile refresh, or generation jobs can fail with must be owner of table ....
Admin console navigation state is intentionally short-lived. src/modules/console/pages/console-dashboard-page.tsx stores the active console view in sessionStorage: page refresh stays on the current admin page, logout clears the stored view, and a new browser tab/session opens the dashboard first.
Storage Architecture
src/lib/local-storage.ts is the storage abstraction. It keeps the public URL shape stable while allowing the backend to move from local disk to object storage. Generated media delivery is layered through src/lib/media-watermark-policy.ts and src/lib/media-watermark.ts so compliance-visible watermarks are applied at the server boundary rather than as a frontend overlay.
- Public URL shape remains
/api/local-storage/<key>for local and object-backed files. Existing DB rows and frontend image URLs do not need rewriting during migration. STORAGE_MODE=local: read/write onlyLOCAL_STORAGE_DIR, or repo-locallocal-storageif unset.STORAGE_MODE=dual: read object storage first, fall back to local disk, write every new file to local disk first, then mirror it to object storage. Object mirror failures are logged instead of breaking the user request. This is the recommended production migration mode because existing local backups and rollback stay useful while object storage is populated.STORAGE_MODE=object: read/write only the configured S3-compatible bucket. Use this only afterscripts/storage-sync-to-object.mjs --verify-onlypasses and rollback expectations are clear.- Object storage config uses
OBJECT_STORAGE_BUCKET,OBJECT_STORAGE_REGION, optionalOBJECT_STORAGE_ENDPOINT, access keys,OBJECT_STORAGE_FORCE_PATH_STYLE, and optionalOBJECT_STORAGE_PREFIX. - Rainyun ROS is handled as a control-plane preparation step, not as a runtime storage backend.
scripts/rainyun-ros-prepare.mjscallsPOST https://api.v2.rainyun.com/product/ros/bucketwithx-api-keyand{ bucket_name, instance_id }, then writes.env.rainyun-object.generatedwith standardOBJECT_STORAGE_*values derived fromaccess_key,secret_key, andpublic_api_url. Copy those values into production.env.localonly after review, keepSTORAGE_MODE=dual, and keep.env.rainyun-object.generatedprivate. - File serving route:
src/app/api/local-storage/[...path]/route.ts. Generated images/videos under generated/gallery/imported work media paths, plus generated/gallery/work thumbnails, are served as watermarked bytes usingpublic/watermark/miaojing-watermark-logo.pngandMIAOJING AIat 50% opacity; this route should not expose raw object-storage redirects for generated media because browser extensions or scripts can call the same URL. For generated image originals that already have a localworks.thumbnail_url, display requests can redirect to the thumbnail and watermark that smaller file first; download requests still go through/api/downloadfor the original media. - Download route:
src/app/api/download/route.ts. Downloads also return watermarked files by default; a raw generated file is allowed only when the request authenticates an admin role or a user whoseprofiles.watermark_disabled=true. Frontend helpers pass the session via Authorization for fetch downloads and a same-origindownloadTokenfor anchor-triggered downloads. - Storage key validation prevents traversal through
normalizeKey,path.resolve, and..checks.
Generation routes persist generated media through the storage adapter. Image originals and video originals are object-first when object storage is configured: images go through src/lib/media-storage.ts, while videos from src/app/api/generate/video/route.ts are stored with uploadFileObjectOnly(...) under generated/videos. Gallery publish uses src/lib/gallery-publish-media.ts: stable /api/local-storage/... image and video originals are reused rather than synchronously copying object-backed generated media again, while external media URLs are copied into gallery storage before insertion. Admin data export/import reads and restores through the same adapter, and import whitelists manifest_path plus system API pricing fields so intelligent API configurations survive server migration. Import preserves auth.users.password_hash and existing encrypted secret fields as encrypted values; production migrations must carry the same DATA_ENCRYPTION_KEY/JWT secret family or encrypted API/payment secrets and existing sessions cannot be decoded correctly. Work dedupe is scoped by user_id plus URL/source URL/media SHA to protect private data ownership when different users have identical media.
Image originals and previews have separate storage rules. src/lib/media-storage.ts persists new generated image originals through localStorage.uploadFileObjectOnly(...) so production originals live in the object bucket even while the app remains in STORAGE_MODE=dual. The same helper writes high-quality compressed WEBP thumbnails through uploadFileLocalOnly(...) under thumbnails/...; the current thumbnail profile uses 1280px max edge, WEBP quality 86, Lanczos resize, and light sharpening, with an m1280q86 filename suffix so older thumbnail profiles can be replaced in the background. Create results, creation history, gallery cards, and gallery detail previews should render thumbnailUrl, while fullscreen preview, download, copy, edit, and share actions must continue to use the original url; the storage/download routes decide whether that original URL returns watermarked bytes or raw bytes. Sharing an already generated /api/local-storage/... image should reuse that original URL and existing thumbnail instead of copying the object into gallery/images or recompressing a gallery thumbnail synchronously; missing/stale thumbnails are backfilled later by /api/gallery reads. Legacy rows without current works.thumbnail_url are queued for background thumbnail backfill by /api/creation-history and /api/gallery when image works are read; list responses must not wait for thumbnail generation. Thumbnail backfill should read object-only originals through short-lived signed object URLs instead of slow SDK buffering. The /api/local-storage/* route reads thumbnails/... from local disk directly instead of probing object storage first and serves them with long immutable browser cache headers because thumbnail filenames include the profile/hash; generated image original display requests can also redirect to that existing thumbnail before watermarking to avoid first-view object GET and full-size raster work. src/proxy.ts must explicitly preserve cacheable thumbnail/gallery routes instead of applying the default /api no-store header. Object-backed generated originals should not return raw signed URLs from public display paths; non-generated object-backed originals can still return a short-lived 302 signed object-storage URL instead of buffering through Next.js.
Video originals and previews also have separate storage rules. Generated videos are stored as object-backed /api/local-storage/generated/videos/... URLs. Video thumbnails are local files under thumbnails/works/videos or thumbnails/gallery/videos, generated by ensureLocalVideoThumbnail(...) when history/gallery rows are written or read. The current preferred profile is a real video frame extracted by ffmpeg-static and stored as video-frame-m1280q86-v1.webp; lightweight SVG profiles such as video-svg-v1 and video-fallback-svg-v2 are only fallbacks when frame extraction fails. SVG fallback rows are treated as stale so gallery/history reads can backfill real frame thumbnails in the background; publish also tries frame extraction before copying any client-provided thumbnail. Object-backed videos are streamed from the storage adapter into a bounded temporary local file before ffmpeg extraction, with retry around transient object-stream termination; this avoids passing signed object-storage URLs directly to the bundled ffmpeg-static binary, which can crash or return no stderr for some remote inputs. Gallery video cards and detail overlays render the thumbnail first; the original video element is mounted only after the user clicks play, so opening the gallery detail does not immediately download the object-storage video. /api/download can redirect non-generated object-backed local-storage downloads to signed object URLs with content-disposition, but generated videos return watermarked bytes unless the authenticated member/admin no-watermark preference allows the raw file; video buttons use a normal anchor-triggered download and pass a same-origin download token.
Gallery detail metadata must not load original images just to compute size. ImageMetadataBadge accepts stored width/height; gallery detail passes those values with loadMetadata={false} so preview surfaces stay thumbnail-only and original requests are reserved for fullscreen, download, copy, edit, and share.
The public gallery page should use server gallery rows only. It must not merge miaojing_published_gallery or miaojing_creation_history from browser localStorage into the gallery feed, and it must not auto-sync historical local published records into Supabase on page load. /api/gallery is the authority for all gallery views, including all/category filters and search, and should only return stable platform media URLs under /api/local-storage/...; legacy external import URLs are not public gallery candidates. Client sharing flows must call /api/gallery/publish first and only then mark local history as shared with publishedAt; stale local published=true without that confirmation must not disable retry. To keep reopen latency low, src/app/gallery/page.tsx caches bounded page data in browser localStorage for instant first paint, uses cached rows up to the 7-day prune window while revalidating page 0 in the background, and shows a masonry skeleton instead of a blocking centered loading message when no cache exists. Public gallery serialization in src/lib/gallery-response.ts filters generated default data: avatars and oversized avatar URLs so repeated publisherAvatarUrl fields do not bloat /api/gallery responses or exceed localStorage quota. It should request small pages and append via IntersectionObserver as the user scrolls, not load the entire public gallery into the DOM.
Admin gallery moderation is separate from the public gallery page. src/components/admin/gallery-management-tab.tsx lists public completed works through /api/admin/gallery/works with page/pageSize pagination; src/lib/admin-gallery-works-pagination.ts keeps the route compatible with older limit/offset callers. Prompt edits go through /api/admin/gallery/prompt and src/lib/admin-gallery-prompt-service.ts. The service enforces the moderation rule that the author notification email must send successfully before works.prompt is updated. Platform logs record the admin, work, author, reason key, prompt length changes, and notification result, but must not store the full original or edited prompt text.
Fullscreen image overlays should accept a thumbnail fallback and display it immediately while the original object-storage image loads. If object storage is slow or the original fails, the user still sees the high-quality local preview and the fullscreen controls stay usable; copy/download/share actions still receive the original URL.
/api/health caches storage health briefly and bounds object bucket probing, so health checks do not block page monitoring on a slow object-storage HEAD request. Optional runtime schema checks cache success or non-owner skips; production migrations should still apply schema changes explicitly, but request paths should not repeatedly run DDL.
For a production move from local disk to cloud server plus object storage, use this order: create a full DB/file backup, run pnpm run migration:check against the source runtime, prepare Rainyun ROS with pnpm run rainyun:ros-prepare -- --create if a bucket still needs to be created, copy reviewed OBJECT_STORAGE_* values into .env.local with STORAGE_MODE=dual, run pnpm run storage:sync-object -- --dry-run, run pnpm run storage:sync-object, run pnpm run storage:sync-object -- --verify-only, deploy/reload, run pnpm run migration:check again, and verify /api/health, gallery/history images, downloads, login, and API generation. The migration checker defaults to http://127.0.0.1:8000 and uses bounded storage URL probe helpers; override MIGRATION_CHECK_BASE_URL, timeout, or concurrency only when intentionally checking a different runtime. Only switch to STORAGE_MODE=object after the object bucket and migration integrity checks have passed and a rollback plan exists.
When syncing source into production, exclude the repo-root runtime storage directory as /local-storage/ only. A broad local-storage/ rsync exclude also skips src/app/api/local-storage/[...path]/route.ts, leaving production on stale file-serving code while the local repo appears fixed.
Database Architecture
Main DB entry:
src/storage/database/local-db.ts: PostgreSQL pool fromLOCAL_DB_URL.
Schema sources:
src/storage/database/shared/schema.ts: Drizzle snapshot for core tables.scripts/init-database.sql: full initialization.- Runtime routes/services also ensure compatible columns/tables with
ALTER TABLE ... ADD COLUMN IF NOT EXISTS.
Core data areas:
- Users:
auth.users,profiles.profiles.watermark_disabledis the per-user authorization for downloading raw generated media. Free users cannot enable it from their own profile, but admins can toggle it in user management without changing membership; platform display still stays watermarked. - Works:
works. - Credits:
credit_transactions,redeem_codes. - Orders:
orders. - API credentials:
user_api_keys,system_api_configs,api_providers. - Site content:
site_config,announcements,site_stats.
site_config.image_composition_skill_enabled is a platform-wide feature switch for the built-in image composition skill. When enabled, image generation uses src/lib/layout-composition-skill.ts to deterministically select one of 100 nevertoday/100-layout-compositions references and append composition-only guidance to the prompt before any upstream image provider call. The source is CC BY 4.0, so admin UI and docs should keep attribution visible; the generation request should use it as layout guidance rather than as downloadable gallery/reference media.
- Jobs/logs:
generation_jobs,platform_logs.
Because several routes self-migrate compatibility columns, DB bugs often require checking both SQL scripts and route-level ensure...Schema functions.
Credit redemption uses src/lib/redeem-code-service.ts. Admin-generated codes are unique single-use rows in redeem_codes; user redemption locks the code row and profile row in one transaction and marks the code used. Credit codes increment profiles.credits_balance and write a credit_transactions record. Membership codes set/extend profiles.membership_tier and membership_expires_at; duration can be configured by day, month, or year. The external mall URL for obtaining codes/upgrading membership is stored as site_config.redeem_code_mall_url, edited from the admin redeem-code tab, returned publicly by /api/site-config, and preserved by data export/import. Invitation rewards use src/lib/invitation-service.ts: each profile owns a stable invite_code, /auth/register?invite=... stores the relationship in invitation_referrals, sets profiles.referred_by_user_id, and grants 50 credits to both users in the registration transaction. These tables and fields are included in admin data export/import and production migration checks so unused/used redemption state and invitation history survive server moves.
User display identity: profiles.nickname is retained as the login username so existing username/phone/email login and works.user_id ownership remain stable. Public display uses profiles.display_nickname, surfaced to clients as nickname; src/lib/user-profile-defaults.ts owns runtime schema creation plus random Chinese nickname/default 3D cartoon avatar generation. Existing users without display_nickname or avatar_url can be backfilled with scripts/backfill-user-display-profile.mjs.
Admin Console Architecture
Admin console UI is split across:
- Page/wrapper:
src/app/console/page.tsx,src/app/console/dashboard/page.tsx. - Module pages:
src/modules/console/pages/console-login-page.tsx,src/modules/console/pages/console-dashboard-page.tsx. - Tabs:
src/components/admin/*. - APIs:
src/app/api/admin/*.
Admin auth flows through the same login endpoint with admin role checks. API routes should use requireAdmin.
Gallery prompt moderation uses requireAdminUser when the route needs the admin user ID for platform logs, and requireAdmin for read-only admin list APIs. The moderation endpoint should fail closed when the work is no longer public, the author email is missing/invalid, the prompt is unchanged, or SMTP sending fails.
Upgrade And Deployment Architecture
Scripts:
scripts/build.sh: builds Next.js and server.scripts/start.sh: production start path.scripts/deploy-or-upgrade.sh: deployment/upgrade automation.scripts/admin-upgrade-runner.mjs: package extraction/build/restart runner.scripts/backup-create.sh,backup-list.sh,backup-restore.sh: backups. Backup creation validatespg_dumpoutput and tar integrity; restore validates archive/dump contents, creates a pre-restore safety dump/copy, uses a single DB transaction, and swaps local storage atomically.scripts/apply-database-patch.sh: DB patch execution.
Runtime:
ecosystem.config.cjsdefines PM2 processes and environment roles.src/server.tsis the custom Node server entry.- Admin upgrade API and UI:
src/app/api/admin/upgrade/route.tssrc/components/admin/system-upgrade-tab.tsx
Production note from the 2026-05-14 update: the reachable SSH endpoint was root@124.174.9.29 -p 5238, while PM2 still served /opt/miaojingAI with Node/PM2 under /data/miaojingAI/node/node-v24.15.0-linux-x64/bin. The live ports were 8000 for web, 8100 for API, and 8200 for console. Do not overwrite production ecosystem.config.cjs with a repository or dev-server copy during rsync-style source updates; it can switch PM2 back to /root/miaojingAI and ports 5000/5100/5200.
When changing deploy/upgrade behavior, validate package limits, disk checks, backup creation, rollback paths, restore safety backups, PM2 restart command, and health checks.
All new development must be designed so the production server can be updated later through the admin console upgrade package flow. Classify every deployable change before handoff:
- Hot update candidate: static/public asset-only payloads that the upgrade runner accepts without restart. Preflight must reject source, dependency, script, lockfile, secret, backup, storage, and runtime paths.
- Cold update candidate: any change involving
src, API routes, server code, dependencies,package.json,pnpm-lock.yaml, DB schema or compatibility migration behavior, environment variables, PM2/runtime config, build scripts, backup/restore scripts, deployment scripts, or generated server assets.
Cold updates must preserve this safety chain: package preflight, disk checks, data backup, source snapshot, build/type verification, PM2 reload/restart, /api/health, and rollback through source restore plus backup restore when needed. When a feature introduces new persistent data, schema expectations, file-storage paths, background jobs, or environment variables, update the upgrade/package notes in this architecture document so future production packages can be prepared safely.
Data Portability
Admin data export/import is a portability layer, separate from the full tar backup scripts:
src/app/api/admin/data-export/route.tsexports database business tables and bundles local-storage media referenced byworksandsite_configunder_media.src/app/api/admin/data-import/route.tsaccepts older DB-only exports and newer media-inclusive exports. Newer exports restore media files to stable sha-based local-storage keys before writing work rows.- Import runs one DB transaction with per-row savepoints, remaps user/work/custom API key IDs, preserves old source URL/media SHA markers in
works.params, and merges repeated imports by URL/source URL/media SHA. - Older exports without
_mediacan restore database rows but cannot recreate missing local files by themselves; copyLOCAL_STORAGE_DIRor use a newer export for full gallery/history image recovery.
Security Boundaries
- API key storage: encrypted in DB, preview-only responses.
- User auth: bearer session token from
src/lib/session-auth.ts. - Admin auth: role must be
adminorenterprise_admin. - Internal generation: protected by
x-miaojing-generation-internal. - Local file serving: must preserve storage key normalization and path traversal guards.
- Browser embedding:
src/proxy.tsowns CSP security headers.frame-ancestorsdefaults to self plus mozheAPI origins and can be overridden withMIAOJING_FRAME_ANCESTORS; if an external ancestor is allowed, omitX-Frame-OptionsbecauseSAMEORIGINwould still block the iframe. - Admin destructive actions: keep environment gates, admin checks, and limits.
Verification Strategy
Use the narrowest useful check, then broaden as needed.
| Change Type | Minimum Verification |
|---|---|
| Docs only | Link/path sanity with rg, git diff --check. |
| TypeScript source | pnpm run ts-check. |
| API route | pnpm run ts-check, route smoke with curl where runtime exists. |
| UI workflow | pnpm run ts-check, pnpm run build, browser/manual or Playwright check if visual. |
| Generation path | pnpm run ts-check, pnpm run build, job create/poll route check, storage result check. |
| Static hot-update candidate | Admin upgrade dry run/preflight, verify runner accepts the payload without restart, then route/static asset smoke check. |
| Cold-update candidate | pnpm run ts-check, pnpm run build, admin upgrade dry run/preflight, backup/rollback readiness, PM2 reload/restart, health checks. |
| Deployment/upgrade tooling | pnpm run ts-check, pnpm run build, package/upgrade dry run, PM2 reload, health checks. |
Known Risk Points
- Some source files import
@/lib/model-display, butsrc/lib/model-display.tsis absent at the audited commit. This can breakpnpm run ts-checkindependent of documentation changes. - Generation routes are large and mix upstream adapter logic, persistence, and job progress. Prefer small surgical edits.
- Several DB schema changes are applied lazily at runtime; verify migration behavior on fresh and upgraded DBs.
- Production/dev servers can have different checkout paths. Always verify PM2 cwd before live edits.
- Admin upgrade touches file system, build, backup, disk, and PM2 state. Treat it as high-risk.