An interactive CLI that stands up a full NYCU SDC Core System on your own machine.
The container build is cut from the recording — it takes several minutes the first time, and is nothing but compiler output. Everything else runs at the speed you see. The credentials typed in are fake. See demo/README.md to re-record it.
- Docker with Compose v2 (
docker compose, notdocker-compose) - git — used to fetch the upstream sources and apply patches
- Network access to GitHub, Docker Hub, the Go module proxy and the npm registry
No Go, Node or database toolchain needed on your side: everything is compiled inside
containers. doctor checks all of the above before anything is downloaded.
Core System spans three repos — api, backend and frontend. Standing it up yourself means aligning versions across them, wiring OAuth, seeding the database, and working around a handful of issues that have not landed upstream. This CLI wraps all of that into one command.
- Checks your environment (Docker, docker compose, ports)
- Fetches backend and frontend at the commits pinned in
versions.lock, then applies the patches underpatches/ - Builds everything inside containers and starts the stack
- Walks you through Google OAuth, or uses a no-OAuth trial mode so you can see the UI right away
- Seeds two sample registration forms as drafts
Grab the binary for your platform. Replace darwin_arm64 with darwin_amd64,
linux_arm64 or linux_amd64 as needed:
curl -Lo core-system-launcher \
https://github.com/NYCU-SDC/core-system-launcher/releases/latest/download/core-system-launcher_darwin_arm64
chmod +x core-system-launcher
./core-system-launchermacOS may refuse to run an unsigned download. Either right-click it in Finder and choose Open once, or clear the quarantine flag:
xattr -d com.apple.quarantine core-system-launcherBuilding it yourself works too, and needs nothing but a Go toolchain:
go build -o core-system-launcher .The first run asks three things — the public port, the admin email, and how you want to log in. Later runs just start the stack.
| Command | Description |
|---|---|
up |
Build and start. The first run walks through setup. Pass --rebuild to force a rebuild |
down |
Stop the stack, keeping all data |
logs [service] |
Follow logs, optionally for backend, frontend or postgres |
status |
Show the state of each service |
doctor |
Diagnose environment problems |
reset |
Drop the database, sources and config, then start over (asks for confirmation) |
Set CORE_SYSTEM_LAUNCHER_HOME to move the working directory away from ~/.core-system-launcher.
A single port is exposed. The frontend gateway serves the static build and reverse-proxies
/api to the backend.
http://localhost:<port> -> frontend (Fastify gateway)
|- / -> static build
|- /api/* -> backend:8080
|- postgres:5432
Same-origin is mandatory. The frontend uses @nycu-sdc/core-system-sdk, an orval fetch
client generated without a baseUrl, so every request goes to a relative /api/... path.
Splitting the origins would mean extra CORS and cookie handling, so that option is
deliberately not offered.
The redirect URI is bound to the port you pick. The CLI prints the exact line to paste into the Google Cloud Console:
http://localhost:<port>/api/auth/login/oauth/google/callback
Create the client as a Web application. If your consent screen is External and still in
Testing, add the account you plan to sign in with to the Test users list, or Google will
block the sign-in with access_blocked.
Changing the port means changing both sides, otherwise you get redirect_uri_mismatch.
If you would rather not set up OAuth first, pick trial mode. It uses the internal login
endpoint the backend exposes when DEV=true, and the CLI hands you a snippet to paste into
your browser console.
Trial mode is for looking around only. Do not use it for anything real.
After the first start, two SDC registration forms are seeded as drafts:
| Form | Contents |
|---|---|
| SDC 註冊表單 - 2025 | 15 sections, 44 questions, 4 sign-up branches |
| SDC 註冊表單 - 2026 | 18 sections, 64 questions, 6 sign-up branches |
They exercise what Core System does beyond Google Forms: condition nodes that decide whether a program's screening questions appear at all, ProseMirror rich-text descriptions, ranking questions and hyperlink fields.
Seeding goes through the API rather than raw SQL, because a form spans four levels of related
records (section, question, choice, workflow) and writing those directly means owning UUID
and foreign-key handling that breaks whenever the schema moves. Forms whose titles already
exist are skipped, so up stays idempotent. Nothing is ever published for you.
seed/forms.json is exported from a running system. To update it, run this against that
system after adjusting BASE, UID and WANT at the top of the file:
python3 seed/export.pyThe export contains no UUIDs or timestamps — nodes reference each other by array index — so it replays cleanly into a fresh deployment. The traversal order is deterministic, so re-exporting only produces a diff when the forms actually changed.
The launcher never commits anything back to the three upstream repos. Every change lives as
a patch under patches/, applied after checking out the pinned commit, so what was changed
stays visible and reviewable.
| Patch | Problem it fixes |
|---|---|
frontend/001-preview-section-descriptionHtml |
Section descriptions never render in the admin preview — the field is dropped when building the section list |
frontend/002-workflow-unanswered-condition |
Everything after the first unanswered condition node is hidden from the structure bar |
frontend/003-admin-auth-refresh-interval |
Admin pages never refresh the access token, logging you out every 15 minutes |
frontend/004-auth-refresh-hardening |
Refresh interval far too short, concurrent refreshes racing the backend's token rotation, and no retry once the token has expired |
frontend/005-sectionedit-sync-questions-ref |
Question edits get overwritten by their pre-edit values on save |
To follow upstream, swap the commits in versions.lock and run up --rebuild. If a patch no
longer applies, the CLI names the one that conflicted. CI verifies every patch against the
pinned commits on each push, so breakage surfaces there rather than on someone's laptop.
~/.core-system-launcher/
├── config.json your settings, including the OAuth secret (mode 0600)
├── src/ upstream sources, checked out and patched
│ ├── backend/
│ └── frontend/
└── deploy/ generated compose.yaml and setup.yaml
Your own project directories are never touched. reset removes this whole tree.
- The initial organization slug is fixed to
SDC. The backend hard-codes that string (case-sensitively) as the default org, and the frontend falls back to it too, so changing it silently breaks default role assignment. - Only
http://localhostis supported. Serving a real domain with TLS means putting your own reverse proxy in front. - Upstream's
require_onboardingis currently hard-coded tofalse, so the onboarding flow is skipped entirely. - Question descriptions cannot embed images: the API has no image field, and the frontend's TipTap editor ships without the Image extension.
