◈ CARDS DESIGN STUDIO← Design library

Hosting the card design library

The design library is live at https://cards.rje.ai/. The repository builds a static public/ directory that nginx serves directly. The deployment script adds one virtual host, obtains its certificate with Certbot, and publishes versioned static releases. The first user-run setup rolled back during its HTTP check; the corrected setup succeeded and was verified on 3 October 2026. New browser prototypes and visual studies use the same content-update command.

What was verified locally

Inspected on 3 October 2026 as user rje; DNS was rechecked after the first setup attempt.

Item Observed state
nginx Ubuntu nginx 1.24.0; service active
Configuration /etc/nginx/nginx.conf includes conf.d/*.conf and sites-enabled/*
Site convention Config files in /etc/nginx/sites-available/, symlinks in /etc/nginx/sites-enabled/
Listening sockets TCP 80 and 443, on both IPv4 and IPv6
Static site precedent games.rje.ai serves a directory directly from nginx
Certificate precedent Most existing sites use Certbot's nginx plugin; idleleague.com uses the webroot plugin and a reload hook
Certbot Version 2.9.0 installed; certbot.timer enabled and active; scheduled twice daily with randomized delay
Required utilities nginx, Certbot, dig, curl, OpenSSL, and rsync installed
Privileges rje belongs to sudo; sudo -n requires a password
New hostname Initially absent; after DNS setup, cards.rje.ai resolves to 73.235.218.61 with no AAAA answer
Existing hostnames rje.ai and games.rje.ai both resolved to 73.235.218.61

The existing hostnames suggest the intended public IPv4 address, but this inspection did not independently establish the server's current external address or router forwarding. Local listeners do not prove reachability from the internet. The initial launch subsequently verified nginx configuration, certificate issuance, and HTTPS. A staging renewal rehearsal remains separate.

DNS and network prerequisites

Create an A record for cards.rje.ai using the same current public IPv4 address as the existing nginx sites. At inspection time that address was 73.235.218.61; verify it is still correct before entering it. A CNAME to an existing hostname that consistently reaches this same server is another option. Use an AAAA record only if working IPv6 routing reaches this nginx instance.

Allow inbound TCP 80 and 443 through the router and firewall to this host. Certificate validation uses /.well-known/acme-challenge/ on port 80; the rest of HTTP redirects to HTTPS after setup. Keep port 80 available for renewals. These requirements follow Let's Encrypt's HTTP challenge documentation and port 80 guidance. Incorrect AAAA records can break validation because Let's Encrypt prefers IPv6 initially; see its IPv6 documentation.

Email is optional. If supplied with --email ADDRESS, it is passed to Certbot as account contact information. Without it, the script uses the installed Certbot 2.9 client's --register-unsafely-without-email flag to permit registration without a contact address. Despite that older flag's name, email is not required to issue or automatically renew the certificate. Let's Encrypt ended expiration notification emails on 4 June 2025. Explicit acceptance of its subscriber agreement remains required through --agree-tos.

Files and publication layout

Repository file Purpose
scripts/deploy-site.sh Read-only plan by default; explicit apply for setup or later releases
scripts/check-certbot-renewal.py Read-only validation of an existing certificate's saved renewal settings
deploy/nginx-http.conf.template Temporary certificate bootstrap host; only challenges are served
deploy/nginx-https.conf.template Final HTTPS static host, challenge route, HTTP redirect
public/ Only this tree is copied to the public server
Server path Purpose
/var/www/cards.rje.ai/publications/TIMESTAMP Current content publisher: complete staged releases owned by rje
/var/www/cards.rje.ai/releases/TIMESTAMP Older setup releases, retained for rollback
/var/www/cards.rje.ai/current Symlink pointing to the current publication
/var/www/cards.rje.ai/acme Stable Certbot challenge directory, outside releases
/var/www/cards.rje.ai/backups/TIMESTAMP Previous config and hook, accessible only to root
/etc/nginx/sites-available/cards.rje.ai Site config managed by the script
/etc/nginx/sites-enabled/cards.rje.ai Symlink enabling this site
/etc/letsencrypt/live/cards.rje.ai/ Certbot certificate and key references
/etc/letsencrypt/renewal-hooks/deploy/cards-cards.rje.ai Reload nginx after renewal of this domain

The web server needs no Node process or application port. New prototype directories can be added under public/ and published alongside the library. Missing files return 404; there is no blanket single-page application fallback. Assets revalidate on requests so design changes are visible. Hidden files are excluded, source symlinks are rejected, and private repository files stay outside the published tree.

First publication

From the repository root, build the library and inspect the plan:

npm run build
./scripts/deploy-site.sh

After DNS and forwarding are ready, run this in a terminal with sudo access:

sudo ./scripts/deploy-site.sh --apply --agree-tos

Optionally append --email 'you@example.com' to provide an account contact address.

The script validates its inputs and existing nginx configuration, refuses unmanaged path conflicts, and checks that the renewal timer is running. Before making changes, setup also checks any existing certificate's saved authenticator and domain webroot. It stages a release, enables the HTTP challenge route, probes that route through the public hostname, requests or reuses the certificate, and tests the final HTTPS config before reloading. It then switches the current symlink and compares the HTTPS response for index.html against the staged file.

The HTTP probe runs from this host. If the router does not support access to its public address from inside the LAN, that probe may fail even when remote access works; diagnose the routing before rerunning. Successful certificate validation establishes reachability from the certificate authority. The final content check uses local nginx with the real hostname and trusted certificate; separately open the public URL from another network.

Certbot uses certonly --webroot, which obtains a certificate without asking the nginx plugin to rewrite other hosts. This follows the official Certbot webroot instructions. Config changes use nginx -t before a graceful reload, following nginx's command documentation.

Later publications and rollback

The account that performs setup through sudo now receives ownership of this site's content directory. Routine publication creates releases in the user-owned publications/ subdirectory. nginx configuration, the ACME directory, certificate keys, and configuration backups remain root-owned. The ownership repair has been applied and unprivileged publishing verified on this host. Earlier installations can use:

sudo chown rje:rje /var/www/cards.rje.ai

The assistant can then build, verify, and publish authorized content updates directly. There is no password prompt or nginx reload for routine publications.

npm run publish

This builds the library, checks its local links, stages the complete tree and switches the symlink without changing nginx or requesting a certificate. Setup and content publishing share a per-site lock. Content publication never reads private certificate files or calls nginx/service administration. It verifies the served index through trusted HTTPS and restores the prior content link on failure. If a command fails before successful verification, it restores the prior symlink and any config or hook changed during that run. A certificate already issued is retained by Certbot, and staging files/backups remain for inspection. A failed first setup removes the newly enabled host.

For a later manual content rollback, replace the timestamp below with the previous release printed by the script:

ln -s /var/www/cards.rje.ai/publications/PREVIOUS_TIMESTAMP /var/www/cards.rje.ai/current.rollback
mv -Tf /var/www/cards.rje.ai/current.rollback /var/www/cards.rje.ai/current
curl --fail --head https://cards.rje.ai/

Content rollback does not need an nginx reload. Keep the previous release until the new library and prototypes have been checked. Releases are retained deliberately; remove obsolete release directories manually when storage warrants it, preserving the current and previous versions.

Certificates and renewal

An existing certificate can be reused only when its saved renewal configuration uses the webroot authenticator and explicitly maps this domain to /var/www/cards.rje.ai/acme. The check uses Ubuntu Certbot's own ConfigObj parser via /usr/bin/python3; both are already installed here. A mismatched or missing renewal configuration stops setup before deployment changes. Supplying a different webroot to certonly --keep-until-expiring is insufficient: a certificate that is not due for renewal can be returned without saving the new settings.

For a separately created certificate, first make the intended challenge directory reachable through its currently serving virtual host. Then migrate the lineage with the supported command and rerun setup:

sudo certbot reconfigure --cert-name cards.rje.ai --authenticator webroot --webroot-path /var/www/cards.rje.ai/acme --webroot-map '{"cards.rje.ai":"/var/www/cards.rje.ai/acme"}'

The explicit map prevents an old domain mapping from taking precedence over the new default webroot. If the certificate also covers other hostnames, include their correct mappings when reconfiguring. Certbot's reconfigure command validates the requested settings against its staging service before saving them; see the Certbot renewal configuration guide. Do not hand-edit the renewal file to bypass the guard. This migration is unnecessary for a fresh domain whose certificate is first created by the deployment script.

Use the existing certbot.timer; the script does not create a duplicate scheduler. The stable ACME directory remains reachable on port 80, and the site-specific deploy hook tests and reloads nginx after certificate renewal. The hook acts only when cards.rje.ai appears in Certbot's renewed domains.

After initial setup, run the targeted renewal rehearsal:

sudo certbot renew --cert-name cards.rje.ai --dry-run --run-deploy-hooks
systemctl list-timers --all certbot.timer

This renewal command contacts Let's Encrypt's staging service and deliberately runs the hook, so it also reloads nginx after a successful check. It is different from the deployment script's read-only dry run. Certbot documents renewal tests and deploy hooks in its renewal guide.

Verification completed and limits

bash -n passed for the deployment script. The default plan was exercised without writes; invalid domain names and a source symlink were rejected. The renewal guard accepted a matching saved configuration and rejected mismatched authenticators, domain mappings, missing files, and malformed files in temporary fixtures. Both nginx templates passed independent nginx -t checks with temporary certificates/log paths and unprivileged ports. This checks syntax without binding the live service ports or reading production private keys.

After the initial rollback, the user reran the corrected setup successfully. HTTPS returned the exact staged library, HTTP redirected to HTTPS, and the served Let's Encrypt certificate covered cards.rje.ai with validity from 3 October 2026 to 1 January 2027. All 17 published files matched the local release at that verification point. Browser controls worked without page errors. Saved renewal configuration, the nginx reload hook, and the enabled renewal timer were inspected; the targeted renewal rehearsal above remains a separate operational check. Later local changes require another publish-only update.

First setup attempt and recovery

The attempt at 19:05 UTC on 3 October 2026 copied the static release successfully. nginx's journal records the bootstrap reload completing at 19:05:00.783 and the rollback reload beginning at 19:05:00.836. The access log records one HTTP 404 for the exact ACME probe path between those reloads. File and directory permissions for the challenge were suitable. This is strong evidence that the immediate check reached the previous configuration before nginx finished its graceful reload; the original curl retry settings did not retry a 404.

After that rollback, the release directory remained but this site's nginx configuration and current link were absent. HTTPS therefore reached another virtual host's certificate and failed hostname validation. DNS resolved to the expected server, and the probe reached nginx through the public address. This failure occurred before the script's Certbot invocation. The successful retry restored the intended site.

The revised script waits for the expected response content after each reload, retrying temporary HTTP failures, certificate mismatches, and stale successful responses for up to eight attempts. It still requires a valid HTTPS certificate and exact published content; persistent failures cause rollback. nginx describes the worker replacement involved in a graceful reload in its configuration control documentation.

The focused regression check, python3 scripts/check-deploy-http.py, passed four local HTTP/TLS cases: an initial 404 followed by correct content, a stale successful response followed by correct content, persistent HTTP failure, and rejection of an untrusted certificate. Shell syntax and the read-only deployment plan also passed. These checks exercise response waiting without modifying the live nginx service.

The successful retry used this corrected setup command from the repository directory:

sudo scripts/deploy-site.sh --apply --agree-tos

Setup is complete. The revised publisher separates server administration from content releases: only setup and certificate administration require sudo. Routine content publishing uses npm run publish as rje.