Deployment Readiness
Nothing in this platform is deployed. This page is the checklist you work through to change that, not a runbook for a running system.
Read it as five independent tracks — the Laravel server, the desktop client, the mobile client, this docs site, and the paperwork — plus one shared blocker that sits above all of them. The server track is the only one where the blockers are severe enough to stop a launch outright.
Blocking issues
Section titled “Blocking issues”Most severe first. Each was verified against the source files named.
Nobody can verify their email
Section titled “Nobody can verify their email”Status: Partial — the code works, the credentials do not exist.
demostatics-web/.env sets MAIL_MAILER=log, so verification mail is written to
the log file and never leaves the machine. Meanwhile routes/api.php puts the
api.verified middleware in front of every write in the JSON API — posts,
comments, polls, votes, moderation bans — plus the two read surfaces that matter
commercially, GET /reports and GET /database-items. The web routes use the
same ['auth','verified'] gate.
The result is that a new account can register and sign in, and can then do nothing but read the free surfaces. There is no code defect here; the mailer is configuration.
GET /api/v1/health answers "can_register": false while this holds.
HealthController::mailIsDeliverable() treats log, array and null as
undeliverable, which is the only honest test — both of those drivers “succeed”
from the application’s point of view.
Fix it by pointing MAIL_MAILER at real SMTP (or a transactional provider) and
setting a MAIL_FROM_ADDRESS on a domain you control. Then confirm /health
flips to "can_register": true.
Nothing drains the queue
Section titled “Nothing drains the queue”Status: Partial — the driver is configured, the consumer does not exist.
.env sets QUEUE_CONNECTION=database and the jobs table exists
(database/migrations/0001_01_01_000002_create_jobs_table.php). No supervised
worker process is defined anywhere in the repository. Jobs are accepted, written
to the table, and never run.
This failure is silent by construction: the dispatching request returns 200. It stays invisible until the first thing that matters is queued — a verification mail, a payment webhook — and simply never happens.
/health reports "queue": "requires_worker". Be clear about what that field
does and does not mean. HealthController::queueRunsJobs() returns true only
when the driver is sync, and its own comment says so:
syncruns inline, so it is always processed. Every other driver needs a worker, and we cannot see from here whether one is running — so this reports the requirement rather than pretending to know.
So requires_worker is a statement about configuration, not a detection of a
dead worker. If you start a worker, /health will still say requires_worker.
You need separate supervision (systemd, Supervisor, or your platform’s process
manager) running php artisan queue:work, and separate monitoring to know it is
alive.
The scheduler is not guaranteed to run
Section titled “The scheduler is not guaranteed to run”Status: Partial — one task is registered, nothing verifies the host invokes it.
routes/console.php registers a daily sanctum:prune-expired --hours=168 with
onOneServer() and withoutOverlapping(). Token expiry itself is live —
config/sanctum.php sets 'expiration' => env('SANCTUM_EXPIRATION', 60 * 24 * 30),
30 days by default — so expired tokens stop working whether or not the prune
runs. What the prune does is stop personal_access_tokens accumulating dead
credential rows forever.
That task only fires if the host installed the cron entry, which the file itself documents:
* * * * * cd /path/to/demostatics-web && php artisan schedule:run >> /dev/null 2>&1Nothing in the repository verifies that entry exists, and its absence fails
silently. On any new deployment, check php artisan schedule:list by hand.
No TLS anywhere
Section titled “No TLS anywhere”Status: Not built.
Every configuration in the tree is local development.
demostatics-web/.envhasAPP_URL=http://localhostandDB_CONNECTION=sqlite..env.exampleshipsSESSION_SECURE_COOKIE=truecommented out.- The mobile client’s default origin is plain HTTP.
demostatics-mobile_application/src/lib/config.tsfalls back tohttp://10.0.2.2:8000on Android andhttp://localhost:8000elsewhere whenEXPO_PUBLIC_API_URLis unset.
Sanctum bearer tokens are sent in the Authorization header on every
authenticated request. In the default configuration those travel unencrypted.
Nothing may be exposed to a network until there is a real certificate on a real
domain and EXPO_PUBLIC_API_URL points at an https:// origin.
The worker tier does not exist
Section titled “The worker tier does not exist”Status: Not built.
GET /reports and GET /database-items are the paid surfaces, and they have no
data producer. The demostatics-backend repository contains one README of two
lines and no code. No other repository contains the worker either. See
the worker tier.
You can deploy demostatics-web without it and everything will respond. What you
cannot do is ship a data product, because the tables behind those two endpoints
are populated by hand today. This blocks launch, not deployment.
Production configuration for demostatics-web
Section titled “Production configuration for demostatics-web”demostatics-web/README.md documents these keys; this table is the deployment
subset. Nothing below has been applied anywhere — treat it as the checklist.
| Setting | What to change | Why |
|---|---|---|
APP_ENV | local → production | Selects production behaviour across the framework and stops development-only paths being taken. |
APP_DEBUG | true → false | With debug on, an unhandled exception renders a stack trace including environment values to whoever triggered it. |
APP_KEY | Generate fresh with php artisan key:generate | Signs cookies and encrypts session payloads. Never reuse a development key. .env is gitignored, so the deployment needs its own. |
APP_URL | http://localhost → the real https:// origin | Builds every link in outbound mail, including the verification link that unblocks the verified gate. Wrong here means unusable verification mails. |
DB_CONNECTION | sqlite → pgsql, mysql, mariadb or sqlsrv, with the matching DB_HOST/DB_PORT/DB_DATABASE/DB_USERNAME/DB_PASSWORD | SQLite is a single file with one writer. The roadmap names Postgres for Phase 0. |
SESSION_SECURE_COOKIE | Commented out → true | Stops the session cookie being sent over plain HTTP. Requires TLS to already be in place, or nobody can sign in. |
SESSION_ENCRYPT | true (.env.example default; the local .env has false) | Session payloads at rest in the store. |
MAIL_MAILER | log → smtp (or a provider driver), with MAIL_HOST/MAIL_PORT/MAIL_USERNAME/MAIL_PASSWORD and a real MAIL_FROM_ADDRESS | The single blocker on can_register. Without it every signup is stranded unverified. |
CACHE_STORE | file/database → a shared store such as redis | The database store works but puts cache traffic on the primary database. A shared store is also what makes more than one app server possible. |
QUEUE_CONNECTION + worker | Keep database (or move to redis) and add a supervised php artisan queue:work | The driver alone does nothing. The process is the missing half. |
| Scheduler | Install * * * * * php artisan schedule:run on the host | Otherwise sanctum:prune-expired never runs and nothing says so. |
| World geo store | Provision per driver | config/database.php attaches world_sqlite only when the default driver is sqlite. On pgsql/mysql/sqlsrv the world store is a real schema or database and WORLD_SQLITE_PATH is ignored. |
The geo store fails quietly
Section titled “The geo store fails quietly”App\Support\GeoData catches QueryException on every accessor and returns an
empty collection, by design: a developer with no world store should get a
rendered page with empty dropdowns rather than a 500. In production that same
behaviour means a misprovisioned geo store looks identical to a world with no
regions in it. GeoData::available() is what /health exposes as
world_store, and it is the only way to tell those apart.
Desktop client delivery
Section titled “Desktop client delivery”Status: Partial. The build and publish pipeline is real and runs today. Everything that makes an installer trustworthy on a user’s machine is missing.
What CI already does
Section titled “What CI already does”Verified against demostatics-pc_application/.github/workflows/ci.yml and
release.yml.
ci.yml runs on pushes to main, on pull requests, on a weekly Monday cron, and
on manual dispatch. Three jobs:
- test —
cargo fmt --all --check(Ubuntu only),cargo clippy --workspace --all-targets -- -D warnings,cargo test --workspace, andcargo bench --workspace --no-run(Ubuntu only). The OS matrix is conditional: pull requests getubuntu-latestalone, while pushes tomain, the weekly run and manual dispatches get Ubuntu, macOS and Windows. The workflow explains the tradeoff — macOS bills at 10x and the workspace has exactly onecfg(target_os)site, inds-platform. - features — clippy over the
guiandremotefeature builds ofdemostatics, clippy overmock-stream-server --features all, and a gRPC backfill integration test. - security —
cargo-denyfor advisories, licenses and bans.
release.yml triggers on tags matching v* and builds four targets:
| Target | Runner | Artifact |
|---|---|---|
x86_64-unknown-linux-gnu | ubuntu-latest | .tar.gz via scripts/package-linux.sh |
aarch64-apple-darwin | macos-latest | .dmg via scripts/package-macos.sh |
x86_64-apple-darwin | macos-latest | .dmg via scripts/package-macos.sh |
x86_64-pc-windows-msvc | windows-latest | .zip, plus a best-effort .msi via cargo-wix |
The shippable binary is the GUI build: cargo build --release --locked -p demostatics --features gui. A final job downloads every artifact, writes
SHA256SUMS over them, and publishes a GitHub Release with generated notes. The
Windows MSI step is continue-on-error on purpose, so a WiX problem never blocks
a release — the zip always ships.
What is missing
Section titled “What is missing”- Code signing certificates. Status: Not built.
scripts/package-macos.shhas acodesign --deep --force --options runtime --sign "$MACOS_SIGN_IDENTITY"step that no-ops when the variable is unset, and nothing in CI sets it. There is no signing step at all on the Windows path. You need an Apple Developer ID certificate and a Windows code-signing certificate before either installer stops being flagged by the OS. - Notarization. Status: Not built. The comment in
package-macos.shsays “Codesign + notarize only when configured”, but only the codesign call exists — there is nonotarytoolsubmission or stapling step. That has to be written, not just credentialed. - Signed auto-update. Status: Planned.
ds-updateris not a crate. Thecrates/directory holds eighteends-*crates andds-updateris not among them; it appears only indemostatics-pc_application/docs/ARCHITECTURE.mdanddemostatics-pc_application/security/README.md, the latter listing update-signing public keys under “Not here yet”. Until it exists, every update is a manual download.
See the desktop client for what the binary itself does.
Mobile release
Section titled “Mobile release”Status: Not built. The app runs in Expo Go against a local server and has never been prepared for a store.
Verified against demostatics-mobile_application:
- No
eas.json. There is no build profile, soeas buildhas nothing to read. - No development client.
package.jsonscripts stop atexpo start,expo export --platform web,typecheck,lintandtest. - No CI. The repository has no
.github/directory, so the Jest suite andtsc --noEmitrun only on a developer’s machine. - Configuration is one variable.
.env.exampledocumentsEXPO_PUBLIC_API_URLand nothing else; the default falls back to plain HTTP on localhost, as noted above.
Start the store paperwork early
Section titled “Start the store paperwork early”The roadmap puts Apple and Google developer accounts and the Paid Applications Agreement in Phase 0, in the non-engineering column, flagged multi-week. They are not hard, but they are gated on someone else’s identity and banking checks and they are worth starting before you need them.
The roadmap also puts in-app purchase last, in Phase 6, deliberately. Revenue comes first from the web via a payment processor, so that first revenue is not sitting behind an app review queue. IAP is added afterwards as an additional sales channel, with the Laravel server still the entitlement authority.
See the mobile client for what is actually implemented.
Docs site
Section titled “Docs site”Status: Not built. This site — demostatics-docs — has no deploy
configuration of any kind: no CI workflow, no Dockerfile, no host configuration
file, no publish script beyond astro build.
What is left is choosing a host and wiring the build. The output is a static site, so any static host works.
See the docs site for how the content is organised, and getting started to run it locally.
Non-engineering prerequisites
Section titled “Non-engineering prerequisites”The roadmap groups these under Phase 0 and runs them in parallel with the engineering work from day one. They have long lead times and they block launch rather than development, which is exactly why they get started before anyone needs them.
The order below changed with the decision to operate as an investment company. Counsel and licensing are now first, and they are first by a wide margin: they are measured in months to years, no amount of engineering shortens them, and they sit on the critical path rather than beside it. Everything that used to head this list — store accounts, the payment processor — is fast by comparison.
| Prerequisite | Status | Note |
|---|---|---|
| Counsel engaged in the intended home jurisdiction | Not built | The longest lead time of anything on this page. Nothing about the advisory, discretionary-management or proprietary-trading lines is answerable until this starts. See Regulatory Posture. |
| Licences for the regulated lines | Not built | Advisory, discretionary management and proprietary trading are separately regulated activities in most jurisdictions. Jurisdiction is itself undecided; if Türkiye is the base the capital markets regulator is the SPK (Sermaye Piyasası Kurulu), but that is a question for counsel and not a decision recorded here. No licence name, threshold or timeline is stated anywhere in these docs on purpose. |
| Legal entity | Not built | Everything below depends on it, and the shape of it is a counsel question in its own right — in particular whether the platform sits inside or outside the regulated entity. |
| Payment processor account | Not built | The roadmap names Stripe. Selling from the web is Phase 3. |
| Public Terms of Service and Privacy URLs | Not built | Required by both app stores and by the payment processor. Once the firm is licensed, what the public site may claim is constrained separately — see Regulatory Posture. |
| Apple and Google developer accounts | Not built | Multi-week. Start before you need them. |
| Paid Applications Agreement | Not built | Separate from the developer account, and separately slow. |
The money-custody question
Section titled “The money-custody question”This section previously carried the standing recommendation never take custody of client funds, repeated without softening. That recommendation is superseded by founder decision. Demostatics will operate as an investment company, and line 3 — discretionary management — means managing client capital. Custody, or a custodian relationship standing in place of it, is in scope. See Business Model and Regulatory Posture.
Two things follow for this page.
Phase 9, money movement, is no longer conditional. It was written as a phase that would happen only if a partner ever came under contract, and probably never. It is now mandatory. What it is gated on has changed too: not engineering readiness, but licensing. The code is the small part.
Whether custody itself is avoidable is an open question for counsel, not a settled answer. Holding only the mandate to trade while an independent custodian holds the assets is materially simpler in most regimes than holding assets directly, and it may or may not be available in whichever jurisdiction is chosen. The safeguarding, segregation, reconciliation, KYC and AML obligations that follow either way come from the regulator. Nothing on this page is legal advice and none of it is a substitute for counsel.
What a regulated deployment adds
Section titled “What a regulated deployment adds”Everything above this section is what it takes to deploy a data platform. A regulated firm has to satisfy a second set of requirements that no amount of uptime, TLS or queue supervision covers. Status: Not built — none of it exists in any repository today, and the current schema was verified for each row.
| Requirement | Status | What is missing |
|---|---|---|
| Record retention | Not built | Nothing in the tree defines a retention period, an archive, or a policy for what is kept and for how long. Retention periods are set by the regulator, not chosen by engineering — they come from counsel. |
| Audit trail | Not built | There is no append-only log of who saw what, who changed what, or when. reports carries published_at and database_items carries measured_at, each plus Laravel’s created_at/updated_at, and neither carries a source. An updated row overwrites the value someone may have acted on. |
| Access control between functions | Not built | App\Models\UserRole defines five roles — user, moderator, admin, technical_staff, editor. Those are forum and editorial roles. Nothing separates a data function from an advisory or trading function, which is what an information barrier is once it stops being a policy document and becomes software. |
| Reconstructing a decision after the fact | Not built | Point-in-time reconstruction — what a figure was at the moment someone acted on it, and how it was derived — needs source, method, method version and append-only revision history. See Business Model for the full schema gap. |
How to tell if a deployment is healthy
Section titled “How to tell if a deployment is healthy”There is one endpoint for this, and it exists because the failures that matter most on this platform are the ones that produce no error.
curl https://your-host/api/v1/health{ "status": "ok", "app": "Demostatics", "api": "v1", "world_store": true, "can_register": false, "queue": "requires_worker"}It takes no token, writes nothing, and is deliberately unwrapped — no data
envelope — because it is metadata about the API rather than a resource, and a
monitor should be able to read it without knowing the resource conventions. It
is documented in demostatics-web/docs/api-v1.md and implemented in
app/Http/Controllers/Api/V1/HealthController.php. See
the JSON API v1 reference for the rest of the contract.
The first three fields tell you the app is alive. The last three exist precisely because the conditions they report are otherwise silent — the API answers 200 on everything while a whole tier is dead.
| Field | Healthy value | What a bad value means |
|---|---|---|
world_store | true | false means the geo store is unreachable. The cascade degrades to empty arrays by design, so without this field “no regions exist” and “the geo store is down” are indistinguishable to a client. |
can_register | true | false means verification mail cannot be delivered from this instance. Every new account will be stranded unverified and locked out of Reports and Database. Signup is broken, however healthy everything else looks. |
queue | processed | requires_worker means the configured driver needs a separate process. Jobs will be accepted and never run. |
Two limits are worth stating so nobody builds a false sense of coverage on this endpoint.
queue reports configuration, not liveness. processed is returned only for the
sync driver, which runs jobs inline. Any production driver reports
requires_worker whether or not a worker is running, because the controller
cannot see the worker from inside a web request. Monitor the worker process
separately.
can_register checks the driver, not delivery. It asserts that
config('mail.default') is not log, array or null. A real SMTP driver with
wrong credentials will report true and still fail to send. Send yourself a
registration mail after any mail change.
Nothing checks whether the scheduler cron is installed. php artisan schedule:list on the host is the only answer available today.