This guide covers building and running the OpenSyria website in an environment that you control. Operations for the hosted service are documented privately.
- Node.js 24+ and pnpm 11+ for a source build, or Docker for a container build.
- An API origin that serves the dataset catalog and released dataset metadata.
- Your own site URL and optional analytics configuration.
Copy .env.example to .env.local for local development. Configure these
values for your own deployment:
| Variable | Purpose |
|---|---|
NEXT_PUBLIC_SITE_URL |
Canonical public URL of your website |
NEXT_PUBLIC_DATASETS_API_URL |
API origin used for dataset metadata |
NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID |
Optional Google tag or Tag Manager ID |
DEPLOYMENT_VERSION |
Release identifier for health checks and deployment skew protection |
The public URL and analytics values are build-time inputs. Rebuild the image
when they change. Never put credentials in NEXT_PUBLIC_* variables.
From the repository root:
pnpm install --frozen-lockfile
pnpm check
pnpm typecheck
pnpm build
pnpm startBuild the standalone runtime image with your own public configuration:
docker build --target runtime \
--build-arg NEXT_PUBLIC_SITE_URL=https://website.example.org \
--build-arg NEXT_PUBLIC_DATASETS_API_URL=https://api.example.org \
-t opensyria/website .Run it locally:
docker run --rm -p 127.0.0.1:3000:3000 opensyria/websiteOpen http://localhost:3000. Set up HTTPS and routing appropriate to your
hosting environment before exposing a deployment publicly.
GET /health reports application health and the non-secret release identifier.
Check the homepage, localized routes, dataset pages, and discovery metadata
after a rollout. The public discovery checks can target your own deployment:
AGENT_READINESS_BASE_URL=https://website.example.org pnpm run test:agent-readinessDataset pages cache API metadata and may be prerendered into the image. After a dataset release, rebuild the website or allow the configured cache lifetime to expire when a short delay is acceptable.
SEO verification covers clean sitemap URLs and parameterized page responses.
Keep query variants separate in any upstream HTML cache: filtered pages carry
X-Robots-Tag: noindex, follow, while clean pages must remain indexable. Preserve
this response header through the hosting layer. After deployment, verify both
admissions languages and the sitemap, then submit the sitemap and inspect the
clean/filtered URLs in Google Search Console. See the README's search-indexing
policy for tracking redirects and the internal _rsc exception.
Hosted service deployment tooling and operator procedures are maintained privately. Use the source and Docker instructions above for your own hosting environment. See releases.md for repository release metadata and image identifiers.
Public documentation covers application behavior, public endpoints, local setup, and reusable hosting requirements. Keep host inventories, private addresses, account identifiers, access rules, credential arrangements, relationships with unrelated projects, and recovery records in private operator documentation outside public repositories.
The site works with manual mark entry when EXAM_RESULTS_ENABLED is unset or
false. Enabling import requires Node.js 24 with node:sqlite, a private decoder
key, a Turnstile widget restricted to your site, and runtime variables from
.env.example. These values are not build arguments or NEXT_PUBLIC_* secrets.
EXAM_RESULTS_SITE_ORIGIN: exact HTTPS browser origin (localhost is supported for development).EXAM_RESULTS_TURNSTILE_SITE_KEYandEXAM_RESULTS_TURNSTILE_SECRET: widget credentials. Only the site key is returned to the browser.EXAM_RESULTS_SIGNING_KEYandEXAM_RESULTS_DECODER_KEY: distinct, Base64 encoded 32-byte keys, supplied through private runtime configuration.EXAM_RESULTS_STORE_PATH: writable SQLite file on a protected local volume. Every website deployment slot must mount the same directory and use the same signing key. The directory must also permit SQLite WAL/SHM files. Do not use ephemeral container storage, a network filesystem, or separate per-slot copies.EXAM_RESULTS_TRUST_PROXY: enable only behind a trusted Cloudflare ingress that suppliesCF-Connecting-IPand cannot be reached directly by untrusted clients. Without a trusted client address, public lookup fails closed.EXAM_RESULTS_METADATA_PATH: optional read-only JSON snapshot of the public exam governorate/curriculum lists for networks that cannot fetch provider metadata. The server validates the fixed exam year and complete governorate coverage before accepting it. Mount it in every slot, review/refresh it when the provider changes its lists, and restart the app after replacing it. Keep snapshot collection procedures and provider-specific payloads private.
The current store supports replicas on one host only. Use a shared transactional
store before distributing application instances across hosts. Endpoint responses
are private/no-store; never cache /api/admissions/results, log request bodies,
or capture student numbers in analytics. Preserve the browser Origin and client
address at ingress. Allow the Turnstile script/frame at
https://challenges.cloudflare.com and the configured provider's browser fetches
in your Content Security Policy. Cookie, challenge and ticket failures must not
fall back to an unrestricted decoding or server-fetch endpoint.
Production checks should cover the real widget, expected hostname/action, manual-entry fallback, both languages, narrow-screen dialogs, private response headers and an explicitly authorized result. Automated tests use synthetic records and injected test services, with no production challenge bypass.