The Catalog of Second Chances (CSC) is a prototypical digital component database. It catalogs digital representations of uniquely identifiable architectural components for reuse. Furthermore, it provides corresponding interfaces and tools to leverage them via a backend REST-API, a web-frontend as well as a interface for direct interaction within Rhino Grasshopper.
- This is research level code! As always: expect bugs, weird behaviour, things not working, etc. pp.
- This is a proof-of-concept / prototype. Things will change (and break) all the time.
- The Catalog consists of a database, backend, frontend, and Grasshopper interface
- We use MongoDB Atlas as database
- The backend is implemented using FastAPI
- We use Python 3.13 (see
src/backend/constraints.txtfor the server's package caps) - The frontend is implemented using the Next.JS framework
- The frontend is designed to connect to the backend on the same server using JWT-based auth
- Grasshopper interface provides Python 3 components for direct integration with Rhino/Grasshopper
- Everything runs on a web server, in our case we use Uberspace
- CSC: 0.6.3.1 --- backend, web frontend and Grasshopper interface are released together
under one version (tag
v0.6.3.1); the single source is theVERSIONfile.
See CHANGELOG.md for release notes.
A complete local setup --- MongoDB, FastAPI backend, Next.js frontend, test suite --- that never touches production. Commands are for PowerShell on Windows; run them from the repository root unless stated otherwise.
-
MongoDB Community Server (runs as the Windows service
MongoDBon port 27017 and starts with Windows):winget install --id MongoDB.Server --source winget Get-Service MongoDB # Status should be RunningThe tests start their own throwaway
mongodfrom this installation; setMONGOD_BINonly if it lives outsideC:\Program Files\MongoDB\Server\. -
Python environment (Python 3.13; backend packages at production versions, test tools, local tools):
conda env create -f csc_env.yml # creates the env "csc" conda activate cscIf an older
cscenv exists, remove it first (conda env remove -n csc). Don'tconda renamean env: pip's.exelaunchers (invoke,pytest, ...) keep the old path and fail with "Fatal error in launcher". Aftercsc_env.ymlor the requirements change:conda env update -n csc -f csc_env.yml(it adds and upgrades; to drop packages, recreate the env). -
Backend settings: copy
src/backend/dev.env.exampletosrc/backend/dev.env(gitignored). To see meshes, previews and photos, point theSNAPSHOT_*_DIRentries at a copy of the production assets. -
Frontend settings: copy
src/frontend/.env.development.local.exampletosrc/frontend/.env.development.local(gitignored).npm run devloads it last, so.env/.env.localcan keep production values. Then install:cd src/frontend npm install -
Local data and an account:
invoke seed --dump 260916 # loads mongodb_collections_local/260916 into the local "csc" database invoke create-user --username me --email me@example.org --admin # prompts for a passwordinvoke seed --dump 260916 --replacereloads a dump over existing data.
| terminal | command | serves |
|---|---|---|
| 1 | invoke dev-backend |
FastAPI on http://127.0.0.1:8000 (API docs at /docs), auto-reload |
| 2 | cd src/frontend then npm run dev |
web app on http://localhost:3000 --- log in with the account from step 5 |
MongoDB needs no terminal: it is the Windows service. The Grasshopper
UserObjects talk to production by default; to use a local backend, set the Server input of
CSC_Session (e.g. localhost:8000; a host without a scheme becomes http:// for localhost, else
https://). Changing it signs you out and clears the cache.
For browser checks open the web app as http://127.0.0.1:<port>, not
localhost: cookies do not separate by port, so a second dev server on another
port (a throwaway invoke dev-local with NEXT_DIST_DIR and
NEXTAUTH_URL=http://127.0.0.1:<port>) would otherwise share its NextAuth
session with the first.
A local copy of the newest dump (for checking the web app against real
data) starts from .claude/launch.json: backend-local (the newest
mongodb_collections_local/<yymmdd> folder, a throwaway mongod on 27791, the
backend on http://127.0.0.1:8791, assets copied to .dev/assets-261012) and
frontend-local (http://localhost:3000). Both run through
scripts/dev/run_csc_python.cmd, which finds the conda environment csc
without a path of your account: CSC_PYTHON (a python.exe you name), the
active environment when it is csc, the conda base from CONDA_EXE, the usual
install folders under %USERPROFILE% and %ProgramData%, then conda run -n csc. The sign-in dev-admin has a new password at every start, written to
.dev/dev-local-credentials-8791.txt. The script is scripts/dev/serve_dump.py
(invoke dev-local runs it too); it loads a dump as it is, and --migrate
runs the 0.6 cutover steps on an old 0.5 dump. A collection with no export file in
the dump is not loaded, and an export file of 0 bytes holds no documents.
invoke test-changed # tier 1, while working: the tests of the changed paths
invoke test-all # tier 2, before a hand-off: everything but `slow`, -n auto; prints the tree hash
invoke test-release # tier 4, before tagging: everything including `slow`
invoke test # plain pytest run (--all: with slow, --parallel: -n auto)
invoke test -k auth # a subset
invoke test --dump 260916 # also the smoke test against a local dump
invoke check-server-wheels # would the backend install on Uberspace 7 without compiling?
Route tests are skipped with a message if no mongod is found. Tests marked
slow (pytest.ini) are left out by default; pytest -m "" runs them too.
-n auto (pytest-xdist) gives every worker its own throwaway mongod; against a
shared server (CSC_TEST_MONGODB_URI) every worker gets its own database
csc_<worker>. CI runs only tests/api, the frontend and the deploy checks.
[next-auth][error][CLIENT_FETCH_ERROR] ... "<!DOCTYPE" is not valid JSON(or "Jest worker encountered child process exceptions" in the dev server output): the Turbopack dev cache is broken. Stopnpm run dev, deletesrc/frontend/.next, start again. Run only one dev server per checkout --- a second one shares and corrupts the same cache.Fatal error in launcherfrominvoke/pytest: the conda env was renamed or moved; recreate it (see step 2) or usepython -m invoke .... Every request to a backend is logged with itsX-CSC-Clientheader inlogs/client_versions.log(locally.dev/logs/). The line holds the time, the client name and version (or the browser's name), the method, the path and the status: no IP address and no user name. Both this file and gunicorn'slogs/fastapi.log(the error log; the access log is off) rotate at midnight and keep 14 days (logrotation.py), the retention the privacy page names. The backend logs through thecsclogger (logging_setup.py): in the API intologs/fastapi.log, in the cron scripts to stderr, which their cron lines append to the dated*_cronjob.<date>.logfiles;CSC_LOG_LEVELsets the level. A log line holds counts and ids only, never a user name, an e-mail address or a typed value. When the database cannot be reached the API answers 503 withRetry-Afterand logs the traceback.
Every environment variable of the backend is named once, with its type, default
and purpose, in src/backend/settings.py; it is the only place that reads the
environment. A missing or wrong value stops the start and names the variable,
never its value.
Start by cloning this repo onto your desktop computer.
Either create a MongoDB atlas account and set up a new database or run a
MongoDB database by other means. You will need the full connection string in
the form mongodb+srv://user:password@host/dbname.
Open a terminal and run the following command to create a random secret key that will be used to sign JWT access tokens while authenticating with the FastAPI backend:
$ openssl rand -hex 32
>> 09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7
- You will end up with a key like the above (DO NOT use the one in this example!).
- If in doubt, have a look here for a detailed explanation of the auth setup: [FastAPI OAuth2 with Password (and hashing), Bearer with JWT tokens] (https://fastapi.tiangolo.com/tutorial/security/oauth2-jwt/#handle-jwt-tokens)
All secrets and configuration are passed via environment variables - no config files with credentials are used in the backend.
The backend reads all configuration from the environment at startup and will exit immediately with a clear error listing any missing variables. There are two places you need to set them:
1. ~/.bash_profile - for the cron jobs and the deploy script: copy the
template uberspaceconfig/.bash_profile.example and fill it in (MongoDB, JWT,
SMTP with the optional SMTP_REPLY_TO, the support address that replies to the
verification and password-reset mails go to, asset folders, CORS origins;
optionally GITHUB_CSC_GH_TOKEN for higher GitHub rate limits in
CSC_Update). Optional: CSC_MAX_DATASETS_PER_USER (default 3), how many
datasets a non-admin account may create; admins are not limited.
Apply immediately by running: source ~/.bash_profile
2. ~/etc/services.d/fastapi.ini - for the supervisord-managed FastAPI
service (supervisord does not read ~/.bash_profile). Copy
uberspaceconfig/etc/services.d/fastapi.ini.example to the server, fill in
the real values in the environment= block, and keep it off git (it is
gitignored; only the .example file is tracked).
- Navigate to
...\csc\src\frontend - Copy
.env.exampleand rename it to.env - Edit
.envand fill in the values following the comments in the file - Proceed in the same way with copying
.env.local.exampleand renaming it to.env.local - Set
NEXTAUTH_SECRETto a long random value of its own: it only encrypts the NextAuth session cookie and is independent of the backend'sJWT_SECRET(the frontend never verifies the backend token). There is noAPI_SECRET; delete it from older.envfiles. - The frontend has no database access and needs no MongoDB variables: the sign-in sends the credentials to the backend's
POST /auth/token, the only place a password is checked. Older.envfiles may keepMONGODB_*; nothing reads them. - The pages carry a Content-Security-Policy built per request with a fresh nonce (
proxy.ts,lib/csp.ts): scripts only by nonce, styles inline, everything else from the app's own origin; the Google map frame and the static host ofNEXT_PUBLIC_STATIC_BASE_URL(images only) are the exceptions. If a page breaks on a server because of it, setCSC_CSP_REPORT_ONLY=1in the frontend.envand restart the frontend: the same policy is then sent asContent-Security-Policy-Report-Only, which only logs violations in the browser console. Remove the line again once fixed. NEXT_PUBLIC_STATIC_BASE_URLis optional and only for public UI files under/static/served by Apache. Leave it empty otherwise. Catalogue files (previews, photos, meshes, point clouds, proxies, capture fixtures) are private: they live outside the web root (~/csc_assets_private/) and are served only through the authenticated/snapshots/...routes of the API. It is ignored on localhost.
The CSC Grasshopper Interface consists of Python 3 components that can be used directly in Grasshopper:
- Location:
grasshopper_userobjects_src/- Source files for development - Installation: Copy
.ghuserfiles fromgrasshopper_userobjects/to your Grasshopper UserObjects folder - Requirements: Python 3 with packages: requests, numpy, scipy, scikit-learn
- Authentication: Use the
CSC_Sessioncomponent first to sign in (see "Signing in from Grasshopper" below) - Documentation: Component reference, copy-to-clipboard XML, and release download on the frontend at
/gh-interface - Backend routes: Release download, sources, XML and updater assets are served under
/ghinterface/(e.g.version,download,src/{name},xml/{name},userobject/{name}). They come from the GitHub release of the running backend (tagv<version>); achannelquery param (GitHub branch or tag) overrides that for testing.
The CSC_Session component has two button inputs, SignIn and SignOut.
- SignIn (Windows, Rhino 8): press it, the browser opens on the CSC
consent page ("Sign in Grasshopper on this computer"), press Allow, and
Grasshopper is signed in; no password goes into a document or an input. The
component listens for the answer on your own computer only (
127.0.0.1, a random port, at most 10 minutes, once) and Grasshopper stays usable while it waits. The sign-in is kept for your Windows user, encrypted with DPAPI, in%APPDATA%\DDU_CSC\signin.json(one entry per server address): after a restart of Rhino the component signs in again by itself. The access token is good for an hour and renews itself through that sign-in, also when a call answers 401; the stored sign-in is good 30 days after its last use and at most 90 days from your consent, then press SignIn again. Every account with a verified address may do it (it is no role). - SignOut: deletes the stored sign-in on this computer and revokes it on the server.
- Revoke from the website: Settings, "Access tokens" lists every sign-in as "Grasshopper on" the name of that computer; "Revoke" ends it (the hour of the current access token runs out; the sign-in cannot renew).
- Username and Password stay as the fallback (and the only way on macOS): when both are connected they win over the stored sign-in. That path renews itself too: when a call answers 401 the component signs in once more with them and repeats the call.
- Pressing SignIn without Windows says that the sign-in without a password is Windows only.
- Changing or resetting your password ends every Grasshopper sign-in of the account too (and every other token and connection): the stored sign-in cannot renew any more, so press SignIn again afterwards.
Backend, web frontend and Grasshopper interface are one product with one
version (VERSION), released together as tag v<version> and deployed by
GitHub Actions. You make every commit, merge and tag; everything after the tag
push is automated.
Cutting a release
- On your working branch, in this order:
invoke bump-version --version 0.5.1.1writes the version everywhere and opens a## [0.5.1.1] - unreleasedsection inCHANGELOG.md. Add--ghif the Grasshopper UserObjects changed --- then re-export the changed.ghuser/ XML in Rhino. With--gh, a client version that already is the new version leavesDDU_CSC_Session.pyas it is (theVersion:header keeps matching the exported.ghuser).- Fill in that
CHANGELOG.mdsection: it becomes the release notes. invoke release-prep(--date YYYY-MM-DD, default today) dates the section, writes.zenodo.json, runs the version check for the tag and the Zenodo check, and runstests/ci. It stops with an error if the section is missing or empty or a check fails, and prints the next steps; a second run only checks.
- Run the whole suite once locally, including the slow tests:
invoke test-release(setCSC_TEST_RHINO=1for the headless Rhino tests andCSC_DUMP_DIRfor the migration on a dump). CI no longer runstests/catalog,tests/grasshopperor anything markedslow, so this run is the check for them. - Open a PR into
main; CI must pass (.github/workflows/ci.yml): backend route tests (tests/api) on MongoDB, server wheel check, deploy-script test, frontend type check / lint / tests / build, Grasshopper checks (a changed component needs a higherVersion:and a re-exported.ghuser+ XML), version consistency. On a PR the frontend and backend jobs run only when their paths changed; CI does not run on a push to a branch. - Merge, then tag the merge commit on
mainand push the tag:git tag v0.5.1.1 && git push origin v0.5.1.1. .github/workflows/release.ymlruns CI again (every job, whatever changed), buildscsc-backend-<v>.tar.gz,csc-frontend-<v>.zip,csc-gh-interface-<v>.zipandSHA256SUMS, attests each of the three bundles (a signed statement that this repository's release workflow built exactly that file), and publishes GitHub Releasev<v>(pre-release if the version has a-suffix).- The deploy job waits for your approval (environment
production), then runscsc_release_deploy.sh v<v>on Uberspace over a restricted SSH key: download the bundles and verify them (SHA256SUMS, thengh attestation verifyfor the backend tar and the frontend zip; the deploy stops ifghis missing or a check fails), unpack to~/csc/releases/<v>/, build a venv only if the requirements changed, switch~/csc/current, restart, health-check (/versionmust report<v>, the frontend must answer) --- or roll back to the previous release by itself.
Grasshopper updates come from the release the server runs: CSC_Update and
the interface download on the web page read tag v<version> of the running
backend. Merging to main publishes nothing; deploying a release updates
backend, web and Grasshopper at once. Testers can still point UPDATE_CHANNEL
in CSC_Update at a branch.
Redeploy, roll back, status: run workflow Deploy by hand with
deploy v<version>, rollback or status; on the server the same commands are
~/csc/bin/csc_release_deploy.sh v<version> | --rollback | --status. Database
migrations are never part of a deploy.
The server runs whatever ~/csc/current points to:
~/csc/
+- releases/<version>/ backend/ frontend/ deploy/ VERSION venv -> ../../venvs/<hash>
+- current -> releases/<version>
+- venvs/<hash>/ one per requirements + constraints content
+- shared/frontend/ .env / .env.local (frontend secrets, linked into releases)
+- shared/logs/ backend and cron logs (linked as backend/logs)
+- bin/ csc_release_deploy.sh, csc_deploy_gate.sh
The one-time setup (Python 3.13, directories, services, cron, deploy key, GitHub
environment) is described step by step in uberspaceconfig/deployment/README.md.
Templates: uberspaceconfig/etc/services.d/ (supervisord),
uberspaceconfig/crontab/ (cron jobs), uberspaceconfig/.bash_profile.example.
Use the official Next.js codemod to upgrade the frontend to a newer version:
# Upgrade to the latest patch (e.g. 16.0.7 -> 16.0.8)
npx @next/codemod upgrade patch
# Upgrade to the latest minor (e.g. 15.3.7 -> 15.4.8). This is the default.
npx @next/codemod upgrade minor
# Upgrade to the latest major (e.g. 15.5.7 -> 16.0.7)
npx @next/codemod upgrade major
# Upgrade to a specific version
npx @next/codemod upgrade 16
# Upgrade to the canary release
npx @next/codemod upgrade canaryUberspace runs services with Supervisor. The templates in
uberspaceconfig/etc/services.d/ run the active release from ~/csc/current:
copy fastapi.ini.example to ~/etc/services.d/fastapi.ini and fill in the
environment= block (it holds the backend's secrets and is gitignored --- never
commit a filled-in copy), copy frontend.ini.example to
~/etc/services.d/frontend.ini, then supervisorctl reread && supervisorctl update. Deploys restart both services themselves.
-
Gunicorn is set up to run the FastAPI backend on Port 8000
-
The Next.js frontend is configured to run on Port 3000
-
The Web Backends have to be set to the port that the apps are listening on!
-
First, we list the active backends:
[user@servername ~]$ uberspace web backend list
/ apache (default)
[user@servername ~]$
- We will not use the default backend, so we delete it
[user@servername ~]$ uberspace web backend del /
The web backend has been deleted.
[user@servername ~]$
- Next we register a subdomain for our FastAPI backend...
[user@servername ~]$ uberspace web domain add api.username.uber.space
The webserver's configuration has been adapted.
Now you can use the following records for your DNS:
A -> 185.26.156.55
AAAA -> 2a00:d0c0:200:0:b9:1a:9c:37
[user@servername ~]$
- Then we add the corresponding web backend for FastAPI
[user@servername ~]$ uberspace web backend set api.username.uber.space/ --http --port 8000
Set backend for api.username.uber.space/ to port 8000; please make sure something is listening!
You can always check the status of your backend using "uberspace web backend list".
[user@servername ~]$
- Lastly, we set our default backend to point to the Next.js...
[user@servername ~]$ uberspace web backend set / --http --port 3000
Set backend for / to port 3000; please make sure something is listening!
You can always check the status of your backend using "uberspace web backend list".
[user@servername ~]$
For further information please refer to the corresponding Uberspace manual and Uberlab guides:
The backend limits logins, registrations and the expensive exports (PDF, CERO)
per client. A visitor reaches the backend through two hops: the Uberspace web
proxy, then the Next.js server (/api/backend, /api/auth, /api/register).
Each proxy appends the address of its peer to X-Forwarded-For; whatever the
browser wrote itself stands on the left of that chain and is never believed.
- Next.js (
lib/clientAddress.ts) takes the addressCSC_PROXY_HOPSproxies from the right (default1, the Uberspace proxy;0= no proxy in front, nothing is passed on) and sends it to the backend as a single-entryX-Forwarded-For. - The backend (
limiter.py) believes that header only when its peer is listed inCSC_TRUSTED_PROXIES(default: loopback127.0.0.0/8,::1) and then takes the rightmost entry that is not itself a trusted proxy. From any other peer the header is ignored and the peer is the client. Signed-in callers of the exports count per user, everyone else per address. conf.pyhands the same list to gunicorn asforwarded_allow_ips, so the server and the limits agree about who may speak for a client; a bad entry stops the start and*is refused.bind = ':8000'stays (the Uberspace web backend connects there).- The counters live in MongoDB (
CSC_RATE_LIMIT_STORAGE; unset, the database ofMONGODB_URI, in the collectionrate_limit_counters, which a TTL index empties), so that every gunicorn worker counts the same requests. If the database does not answer, requests pass uncounted and the log says so once;memory://keeps the counters in the process (tests, local runs). Failed sign-ins are counted per account as well: ten in fifteen minutes, then 429 withRetry-After, until the window is over. - Request bodies are capped before a route reads them: the mesh and point cloud
uploads at
GEOMETRY_UPLOAD_LIMIT_MB, evidence attachments atEVIDENCE_UPLOAD_LIMIT_MB, photos atSNAPSHOT_PHOTO_UPLOAD_LIMIT_MB(each plus 1 MB for the multipart framing), every other route atCSC_JSON_BODY_LIMIT_MB(default 16). The result route of the remote runner has its own limits. - Set
FASTAPI_URL=http://127.0.0.1:8000on the server: the backend then sees the web server on loopback. If the web server reaches the backend through the public API host instead, the connection comes from the web proxy and the web server's own address is appended to the chain: add both toCSC_TRUSTED_PROXIES, and check after deploying that two visitors do not share a limit (the Uberspace manual does not state which headers its proxy sets or from which address it connects).
A personal access token lets a tool other than the web app (for instance an AI assistant through the MCP layer) read the catalog as its owner, and nothing more. It is not the JWT of a sign-in: a token only works where its scope says, and a JWT does not work in place of it.
- Who. Administrators at first.
CSC_PAT_ROLES(defaultadmin, a comma list of the global rolesadminanduser) opens it to more; the role is checked again on every request, so an account that loses it stops working at once. Everyone sees and revokes the tokens they hold in Settings, "Access tokens"; administrators see every token under Admin, "Access tokens", and can revoke any. - Create. Settings, "Access tokens": a name and a lifetime of 30 days, 90
days (the default) or 1 year; there is no token without an expiry. The token
(
csc_pat_and 43 characters) is shown once, with a copy button; copy it then. The list shows only its first 8 characters, name, scope, created, last used and expiry. At most 10 active tokens per account. - Renew. Within 14 days of the expiry (and after it) the list offers "Renew": a new token with the same name is made and shown once, the old one is revoked.
- Revoke. "Revoke" in the list stops the token at once. Revoked and expired tokens, a lost role, an unverified, disabled or deleted account all stop it at once: the database is asked on every request, nothing is cached.
- Try a token.
curl -H "Authorization: Bearer csc_pat_..." https://<api host>/auth/token-checkanswers the owner, the scope and the expiry; every other answer is the same 401, whatever the reason. This path and/mcpneed noX-CSC-Clientheader (the connectors of Claude cannot send one); every other route still does. - Limits and the log. 120 requests a minute per token (counted per worker, like the read limits); the last use is written at most once a minute and not at all while the maintenance flag is set; creating, renewing and revoking are writes and get the 503 of that flag. The log has one line per use: the first 8 characters of the token and the route, no personal data.
- Stored. Only the SHA-256 of a token is stored (collection
access_tokens), so a leaked database holds no usable token. - Password. Changing your password (Settings) and resetting it by mail end every token and connection of the account at once, of every kind: personal access tokens, Claude connections and Grasshopper sign-ins, their refresh tokens included. Afterwards make a new token, press Allow in Claude again and press SignIn in Grasshopper again.
Claude can read the catalog as you: ask it to find components, show one with its
versions and measurements, compare, or look at what happened to a piece. It
only reads, and it sees what you see on the website; nothing it can reach is
private to anybody else. Administrators can connect it for now (the roles are
CSC_PAT_ROLES).
Connect. In Claude, add a custom connector (claude.ai: Settings, Connectors,
Add custom connector; Claude Code: claude mcp add --transport http csc https://<api host>/mcp) and give it the address https://<api host>/mcp, for
the production server https://api.2ndchances.build/mcp. Claude opens the CSC
consent page: sign in if asked, check the address it says you will be sent back
to (for Claude on the web that is claude.ai), and press Allow. Deny, or closing
the page, connects nothing. Nothing is remembered: every connection asks again.
Allow only connections you started yourself: anyone can start an authorization
and send you the link, so look at the large address line (the host you will be
sent back to) before you press Allow, and press Deny when it is not the tool you
just connected.
What it can do. Nine read-only tools: search_components (the filters of
Browse, a page at a time), get_component, list_versions, get_version
(geometry as counts and a link, never the meshes), get_evidence,
list_datasets, get_dataset, get_timeline and similar_components (the
nearest neighbours on the similarity map, with their distance). Every answer
carries the web link of the record, and a record that is not public is marked
(visibility private, status other than published). There are no write tools
and no tool for users, invitations or tokens; no answer holds an e-mail address
or an account id.
How long it lasts. The connection gives Claude an access token of one hour and a refresh token that Claude renews by itself: the refresh token is good for 30 days after its last use and is replaced on every use (using an old one ends the connection), and a connection never lasts more than 90 days from your consent, then Allow again. Each connection is a line under Settings, "Access tokens" (and under Admin for administrators): "Revoke" ends it at once, access and refresh. An account that loses its role, is disabled or is deleted stops working at once, because every call asks the database.
For scripts and other clients. A personal access token (Settings, "Access
tokens") works on /mcp too, as Authorization: Bearer csc_pat_..., for a
client that sends a header and cannot sign in through a browser.
On the server. CSC_PUBLIC_API_URL must be the API address (https://<api host>); the MCP endpoint and the OAuth server (/.well-known/..., /register,
/authorize, /token, /revoke, /mcp) are served by the backend and are off
without it. During maintenance (the read-only flag) /mcp and the refreshing of
tokens keep working; a new connection, a new registration and a revoke wait. The
limits: 120 requests a minute per token, and per address on registering,
authorizing and the token endpoint.
2ndchances.build is the canonical frontend origin; The old address ddu.uber.space
stays registered and redirects to it. The app cannot be served on both origins
at once: NextAuth v4 resolves every absolute auth URL from the single
NEXTAUTH_URL, and its session cookie is host-only, so a login on the
non-canonical host would set a cookie there and then be redirected away from
it. The redirect lives in src/frontend/next.config.ts and matches on the
request host.
Register the domain and point it at the frontend port:
[user@servername ~]$ uberspace web domain add 2ndchances.build
[user@servername ~]$ uberspace web backend set 2ndchances.build/ --http --port 3000
Then update these values and restart both services:
| Value | Location |
|---|---|
NEXTAUTH_URL=https://2ndchances.build |
~/csc/frontend/.env |
FRONTEND_URL="https://2ndchances.build" (verification email links) |
~/etc/services.d/fastapi.ini and ~/.bash_profile |
CSC_PUBLIC_API_URL="https://api.2ndchances.build" (optional: the API address in the links of the JSON-LD and PDF exports) |
~/etc/services.d/fastapi.ini and ~/.bash_profile |
FASTAPI_CORS_ORIGINS (add the new origin) |
~/etc/services.d/fastapi.ini and ~/.bash_profile |
CSC_WORKERS="2" (optional, 1 to 8, default 2: gunicorn workers; each loads the geometry libraries, so the number follows the memory of the account) and CSC_LOG_LEVEL="info" (optional: debug, info, warning, error, critical, default info) |
~/etc/services.d/fastapi.ini (read by conf.py; a deploy no longer resets them) |
Access-Control-Allow-Origin allowlist |
~/html/.htaccess |
NEXT_PUBLIC_STATIC_BASE_URL (public UI files under /static/ only) describes
where those files are fetched from, not where the app is served, so it only
changes if the Apache static host itself moves. Catalogue files have no host:
next.config.ts allows no remote images, they come through the API.
Adding the domain to FASTAPI_CORS_ORIGINS is a safety net rather than a
requirement: the browser only ever calls the API through the same-origin proxy
at /api/backend/[...path], which is a server-to-server request and therefore
not subject to CORS.
The repo contains uberspaceconfig/html/.htaccess which must be placed at
~/html/.htaccess on the server. It sets CORS headers for the Apache layer and
forces .wsc files to download as application/octet-stream rather than being
served inline. Without it, Grasshopper component downloads will not work
correctly.
[user@servername ~]$ cp ~/csc/uberspaceconfig/html/.htaccess ~/html/.htaccess
Before deploying, update the origin allowlist in the SetEnvIf Origin line to
match your actual frontend domains. A plain Header set Access-Control-Allow-Origin
can only name a single origin, which is why the file reflects a matched origin
instead.
The six SNAPSHOT_*_DIR folders (previews, photos, meshes, point clouds,
proxies, capture fixtures) live in ~/csc_assets_private/, outside the web
root, next to the evidence attachments, and are served only by the API. Two
more .htaccess files keep the old location closed and the public UI files
open:
[user@servername ~]$ cp ~/csc/uberspaceconfig/html/csc_assets/.htaccess ~/html/csc_assets/.htaccess
[user@servername ~]$ cp ~/csc/uberspaceconfig/html/csc_assets/static/.htaccess ~/html/csc_assets/static/.htaccess
~/html/csc_assets/.htaccess denies everything (Require all denied) as a
second guard in case a folder is ever put back; static/.htaccess re-allows
the public UI files in ~/html/csc_assets/static/ (the Grasshopper interface
images the release deploy copies there).
~/csc/bin/csc_maintenance.sh on | off | status shows a static 503 page for the
web domain while the frontend is stopped or deployed (cutovers, migrations); see
uberspaceconfig/deployment/README.md, "Maintenance mode" (the script uses the
domain's own folder /var/www/virtual/$USER/<domain>, creates it when missing and
removes it again, links www.<domain> to it, restores the backends it found, and
never writes to ~/html; verify it on a test subdomain before the first use).
Read-only mode: touch ~/csc/shared/READ_ONLY before and rm ~/csc/shared/READ_ONLY after every maintenance step that writes to the database
or the asset folders (restore, purge, migration, cutover). While the file
exists the backend answers every write with 503 (read-only: maintenance),
sign-in stays open and writes nothing, the web app shows a banner and disables
its write buttons, and the cron jobs that write skip their run. Every worker
checks the file per request, so no restart is needed. Details:
uberspaceconfig/deployment/README.md, "Read-only mode".
All jobs run the active release (~/csc/current) and log to ~/csc/shared/logs/,
each to a file of the day (<job>_cronjob.<YYYY-MM-DD>.log); one more line of the
crontab deletes those older than 14 days (the privacy page promises this).
The complete crontab, ready to paste into crontab -e, is
uberspaceconfig/crontab/crontab.example (it replaces the whole crontab; the
MAILTO line at its top names the maintainer's address, set only on the
server). Its jobs:
| job | schedule | what |
|---|---|---|
| geometry runner | every 20 min (flock -n ~/csc/shared/locks/heavy.lock, --due --limit 10: only the snapshots a write has marked) and nightly at 3:30 (flock -w 900, all snapshots, --limit 200 --sweep) |
geometry runner: frame, shape class, proxies + deviation maps, descriptors, complexity, previews of every snapshot whose derivation is stale. With CSC_GEOMETRY_HEAVY_STAGES=remote it runs only frame and shape class, and the rest runs on a worker elsewhere (main_geometry.py --remote <url> --user <admin>, password in CSC_API_PASSWORD); the recompute route then only marks the heavy stages stale |
| component map | daily 4:15 (flock -w 3600 ~/csc/shared/locks/heavy.lock) |
precomputes PCA / UMAP layouts for the component map |
usermaintenance.py |
daily 2:00 | removes unverified accounts older than 7 days (the log holds counts and ids, no names or addresses) |
| log retention | daily 1:30 | find deletes the cron and deploy logs older than 14 days |
geometrymaintenance.py |
daily 3:00 | removes geometry folders without a component |
The geometry runner and the component map share one lock, ~/csc/shared/locks/heavy.lock
(not in /tmp, which other accounts on the host can write to; mkdir -p ~/csc/shared/locks once),
so the two heavy jobs never run together (server memory): the
frequent geometry run skips its run while the lock is held, the nightly run and
the map wait up to 15 minutes instead of losing their slot. A write that changes
what the geometry stages read (a new version, a file, the geometry, the colour,
an override, the function of the piece) marks the snapshot (derivation_due);
the frequent run derives only marked snapshots, the nightly run checks all of them
(main_geometry.py --due / without it).
Each line starts with source ~/.bash_profile && so the job sees the backend's
environment variables. To run a job by hand:
~/csc/current/venv/bin/python ~/csc/current/backend/main_geometry.py --snapshot <id>
(--help lists the stages and --recompute). Only the cron passes --sweep, which
deletes the previews and deviation maps of snapshots that no longer exist: never
run it against a local copy of the assets.
This project uses OpenAPI schema generation to keep frontend TypeScript models in sync with backend Pydantic models.
- Backend: Pydantic models are enhanced with OpenAPI documentation and Field descriptions
- Schema Endpoint:
/schema/componentendpoint exposes the ComponentModel schema - Frontend Generation: Script fetches schema and generates TypeScript interfaces
- Auto-sync: Models are automatically kept in sync
# Generate data models from backend
npm run generate:models- Update Backend Model: Modify Pydantic model in
src/backend/apps/catalog/models.py - Restart Backend: Restart FastAPI to regenerate OpenAPI schema
- Generate Frontend Models: Run
npm run generate:models - Use Generated Models: Import from
src/generated/ComponentModel
// Instead of importing from components/common/models
// import { ComponentData } from '@/components/common/models';
// Import from generated models
import { ComponentModel, ComponentType, ComponentComplexity } from '@/generated/ComponentModel';
// Use the generated interface
const component: ComponentModel = {
_id: "uuid",
type: "slab",
material: "concrete",
// ... other properties
};src/frontend/
+-- scripts/
| +-- generate-models.ts # Generation script
+-- generated/ # Auto-generated models
| +-- ComponentModel.ts # Generated ComponentModel interface
| +-- index.ts # Export index
+-- package.json # Contains generate:models script
See "Local development and tests" above: invoke test runs the Python tests;
the frontend is checked with npx tsc --noEmit and npm run lint in
src/frontend. CI runs the route tests and the frontend checks on a PR (see above for the rest).
Note: these imports aare used for validation within a doctoral thesis at the Digital Design Unit (DDU), TU Darmstadt. They validate the CSC data model against precedents: other researchers' component databases, each recorded with its own schema and purpose. Importing them as published, inventing nothing and documenting every assumption, shows what the model can represent and what it cannot.
| Dataset | Source | Tools and instructions |
|---|---|---|
| ReCreate pilot elements: 1150 reclaimed precast concrete elements of three donor buildings | https://doi.org/10.5281/zenodo.16809738, CC BY 4.0 | scripts/import/recreate/ |
| EPFL stones for augmented stacking: 452 scanned quarry offcut stones | https://doi.org/10.5281/zenodo.7180964, MIT | scripts/import/epfl_stacking/ |
The data is fetched from Zenodo and never committed. Everything else (environments, the order of the steps, the local rehearsal, the production procedure, attribution) is in each folder's README.
Bug reports, requests and pull requests are welcome; see CONTRIBUTING.md and the Code of Conduct. Report vulnerabilities privately as described in SECURITY.md.
Part of this research was conducted within the project Fertigteil 2.0 - Real-digital process chains for the production of built-in concrete components (Precast Concrete Components 2.0), funded by the German Federal Ministry of Education and Research (BMBF, today BMFTR) through the funding measure "Resource-efficient circular economy - Building and mineral cycles (ReMin)".
Part of this research is conducted within the project ZirKuS - Circular Construction and Structural Design of Reused Concrete Components, funded by the Deutsche Bundesstiftung Umwelt - DBU (German Federal Environmental Foundation, project 40160-01) within the funding line "Climate- and Resource-Efficient Construction".
- The
csc_labelspython code to create QR-Code labels was developed by Mirko Dutschke. The code has been refactored as a python module and integrated by Max Benjamin Eschenbach. - The
csc_sheetscanpython module was developed based on the scanning setup for sheets that was developed by Mirko Dutschke. The functional code has been written by Max Benjamin Eschenbach. - Idea and prototype code for
FindLargestFlatSideGrsshopper component by Alessandro Garruto. The code has been refactored and integrated by Max Benjamin Eschenbach. Idea and prototype code forMaxInscribedQuadGrasshopper component by Alessandro Garruto. The code has been refactored and integrated by Max Benjamin Eschenbach.
- Original code is licensed under the MIT License.
- The
csc_sheetscanmodule makes heavy use of the OpenCV library, more specifically its pre-built packages for python via conda-forge.
- The technical main inspiration for the Catalog of Second Chances interface is the Catalog Explorer by @AymbericBr.
- Another huge inspiration and reference is the Timberstone Project, which is the origin of abovementioned Catalog Explorer.
When using, extending or building upon this piece of softare in your work, please reference it accordingly:
@software{eschenbach_2026_20156667,
author = {Eschenbach, Max Benjamin},
title = {Catalog of Second Chances (CSC) - Digital Database
and Corresponding Interfaces for ReUse of
Architectural Components
},
month = may,
year = 2026,
publisher = {Zenodo},
doi = {10.5281/zenodo.20156666},
abstract = {The Catalog of Second Chances (CSC) provides a prototypical platform and the corresponding tools to leverage a database of uniquely identified, digitized architectural components for creating designs that reuse these components.},
url = {https://doi.org/10.5281/zenodo.20156666},
}
Find pre-written citations in the style of your choice over at Zenodo (Citation box on the right side).
