We're using the Next.js framework for its quick development opportunities and rich open-source community. The site runs as a real Node server (next build + next start) on port 3000 on GPU4, behind Apache. Our chat, session, and user data comes from the Django backend.
There are three backend services on GPU4, and Apache (/etc/apache2/sites-enabled/chatdku.conf) decides which one gets each prefix. It terminates TLS and fans out:
| Path | Goes to |
|---|---|
/api/chat, /api/c/, /api/feedback, /api/events |
Django, 127.0.0.1:8009 |
/api/get_session |
Django /api/c/create_session (an Apache-level rewrite) |
/user, /admin |
Django, 127.0.0.1:8009 |
/public/chat, /public/auth/get-token |
FastAPI public, 127.0.0.1:8999 |
| everything else | this Next server, 127.0.0.1:3000 |
Note the vhost rule is ProxyPass /user/ http://127.0.0.1:8009/user/ — balanced on both sides. /user/upload therefore reaches Django intact, while bare /user does not match the rule and falls through to the Next catch-all, where app/user/route.ts proxies it on. Both work; they just take different routes. Don't add a trailing slash to API_ENDPOINTS.USER to "fix" the asymmetry — see the comment in lib/constants.ts for the history.
Shibboleth guards /api/login and nothing else. It is not enforced on the app, on /api/*, or on /user — so this app is served to signed-out visitors and has to notice.
components/AuthGate.tsxwraps the chat pages. It probes/useron load and, on a 401, redirects to/api/loginwith a 15-second guard against a redirect loop.- Django's 401 body carries
login_url. Client-side fetches that can outlive a session route throughhandleUnauthorized()inlib/auth.ts. - The credential is Django's
sessionidcookie,HttpOnly, with a one-week absolute expiry. Not rolling: an active user still re-authenticates weekly. chatdku_session_idis not authentication despite the name — it is the open conversation id and expires after a day.- Apache reaches Django directly, so the route handlers under
app/api/andapp/user/only run in development. They are still written as faithful proxies that mirror Django's URLs 1:1 (seelib/server/backend.tsfor the full contract), so dev and production behave the same and a change to the Apache config cannot silently start serving mock data. Mock responses only appear whenMOCK_APIis on, which is the default fornpm run dev.
We're using the shadcn/ui open-source UI library. This is a widely used, simple, and customizable UI library that uses Tailwind CSS for globally consistent styling.
Try to stick to these shadcn/ui components as much as possible, and only create custom components when necessary. This is to keep accessibility standards and consistency.
- The latest Node.js LTS runtime must be installed on the machine you're using to develop.
- Run
npm installin the frontend directory to install Node dependencies. - Run
npm run devto spin up a localhost server and navigate to http://localhost:3000/ to see the homepage. The dev server will hot-reload whenever you save. - Make necessary edits, and review changes on both a desktop screen and a mobile screen. Test with many aspect ratios to make sure nothing clips or looks broken.
npm run devserves mock chat responses with markdown in them, so check that responses stay clear and legible (this is important — users must be able to read ChatDKU's answers easily). SetMOCK_API=falsein.env.localto hit the real backend instead, which needs internal network access. - Use
npm run testto run the suite (npm run test:watchwhile working,npm run test:coveragefor a report). - Check that
npm run typecheck,npm run lintandnpm run buildall succeed before pushing to the main branch.
Tests run on Vitest with Testing Library, split into two projects:
- ui — components and browser-side
lib/code, in jsdom. - api — the route handlers under
app/, in node, against realRequest/Responseobjects.
Because the route handlers are proxies, the useful seam to stub is Django itself, not our own
endpoints. integration/chat-flow.test.tsx does exactly that: the component's fetch calls are
routed into the actual route handlers, and only the backend beyond them is faked, so a mismatch
between the client, the proxy and the documented backend contract fails the suite.
When you change an endpoint, update the contract notes in lib/server/backend.ts and the fake
backend in the integration test together — they are the two places that describe what Django
returns.
The app runs on GPU4 as chatdku-web.service, a systemd unit that keeps next start alive on
127.0.0.1:3000 and brings it back after a crash or a reboot. The unit file lives at the root of this
repo; /etc/systemd/system/chatdku-web.service is a copy of it. Deploying means rebuilding in place
and restarting the unit:
sudo touch /etc/apache2/maintenance.flag # see below
cd /opt/chatdku/ChatDKU-web
git pull
PATH=/opt/node-22/bin:$PATH npm ci # only if the lockfile changed — see below
PATH=/opt/node-22/bin:$PATH npm run build
sudo systemctl restart chatdku-web
sudo rm /etc/apache2/maintenance.flagPull as yourself — the checkout is owned by a maintainer (anar) and shared through the deploy group, not
owned by chatdku-admin (see first-time setup below for why). The PATH prefix is there because
the system node on GPU4 is 18, too old to build this; put /opt/node-22/bin on your PATH in
~/.bashrc if you deploy often.
Take the maintenance window. next build rewrites .next underneath the live server, so there
is a window where a user can get a chunk mismatch. The vhost serves a maintenance page whenever
/etc/apache2/maintenance.flag exists, and its RewriteCond re-checks that file on every request —
so no Apache reload is needed to raise or lower it.
Build before restarting, not after: next start serves whatever is in .next at the moment it
boots, so restarting first would put the old build back up and then swap it out mid-flight.
If this deploy changes the auth path, the ordering against Apache matters — the frontend must
ship before the vhost is ungated, never the reverse. See SHIB_NARROWING_RUNBOOK.md §4.
Afterwards, visit ChatDKU in incognito mode. Make sure a chat response streams in and is clear and legible.
Useful commands:
systemctl status chatdku-web
journalctl -u chatdku-web -f # live logs
journalctl -u chatdku-web -n 100 # last 100 linesRollback: there is no build backup to restore —
git checkout <last-good-commit>, rebuild, and restart the unit.
These steps are only needed once. Everything runs as chatdku-admin, the user the other ChatDKU
services on GPU4 already run as.
1. Install a supported Node runtime. GPU4's system node is 18, and Next 16 needs 20.9 or
newer. Rather than upgrading the system package out from under the other services on that box,
unpack the LTS tarball into /opt/node-22, which is the path the unit's PATH points at:
curl -fsSLO https://nodejs.org/dist/v22.22.2/node-v22.22.2-linux-x64.tar.xz
sudo mkdir -p /opt/node-22
sudo tar -xJf node-v22.22.2-linux-x64.tar.xz -C /opt/node-22 --strip-components=1
/opt/node-22/bin/node -v2. Clone the repo to /opt/chatdku/ChatDKU-web, alongside the backend checkout, as your own
user — not as chatdku-admin:
git clone git@github.com:Edge-Intelligence-Lab/ChatDKU-web.git /opt/chatdku/ChatDKU-webThe service runs as chatdku-admin, but the checkout deliberately is not owned by it.
chatdku-admin has no credentials on GitHub, so a checkout owned by it could never be
git pulled; and running git against a repo owned by another user trips Git's dubious-ownership
guard anyway. Instead the whole tree is shared through the deploy group, which every maintainer
belongs to. /opt/chatdku is setgid with group::rwx, so anything created inside it inherits group
deploy, and the standard umask 002 on this box makes it group-writable. ChatDKU-backend next
door works exactly this way — owned by a person, served by chatdku-admin.
Two things follow. Keep your umask at 002 when you build, or .next comes out group-read-only and
the service cannot write its runtime cache. And if you clone as a user who is somehow not in
deploy, fix the group rather than the owner:
sudo chgrp -R deploy /opt/chatdku/ChatDKU-web
sudo chmod -R g+w /opt/chatdku/ChatDKU-web3. Build once:
cd /opt/chatdku/ChatDKU-web
PATH=/opt/node-22/bin:$PATH npm ci
PATH=/opt/node-22/bin:$PATH npm run build4. Add /opt/chatdku/ChatDKU-web/.env.production if this deployment needs to override anything
(BACKEND_BASE_URL, for instance). The file is optional — the unit starts without it, and
NODE_ENV=production already keeps MOCK_API off. Keep it out of git.
5. Install and enable the unit:
sudo cp /opt/chatdku/ChatDKU-web/chatdku-web.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now chatdku-web
systemctl status chatdku-webenable --now both starts it and makes it come back on reboot. Confirm it is actually listening
before you go looking at Apache:
ss -tlnp | grep 3000
curl -I http://127.0.0.1:3000If you edit chatdku-web.service in the repo, the copy under /etc/systemd/system/ does not change
by itself — copy it over again and daemon-reload.