Skip to content

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.

Most severe first. Each was verified against the source files named.

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.

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:

sync runs 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.

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:

Terminal window
* * * * * cd /path/to/demostatics-web && php artisan schedule:run >> /dev/null 2>&1

Nothing in the repository verifies that entry exists, and its absence fails silently. On any new deployment, check php artisan schedule:list by hand.

Status: Not built.

Every configuration in the tree is local development.

  • demostatics-web/.env has APP_URL=http://localhost and DB_CONNECTION=sqlite.
  • .env.example ships SESSION_SECURE_COOKIE=true commented out.
  • The mobile client’s default origin is plain HTTP. demostatics-mobile_application/src/lib/config.ts falls back to http://10.0.2.2:8000 on Android and http://localhost:8000 elsewhere when EXPO_PUBLIC_API_URL is 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.

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.

SettingWhat to changeWhy
APP_ENVlocalproductionSelects production behaviour across the framework and stops development-only paths being taken.
APP_DEBUGtruefalseWith debug on, an unhandled exception renders a stack trace including environment values to whoever triggered it.
APP_KEYGenerate fresh with php artisan key:generateSigns cookies and encrypts session payloads. Never reuse a development key. .env is gitignored, so the deployment needs its own.
APP_URLhttp://localhost → the real https:// originBuilds every link in outbound mail, including the verification link that unblocks the verified gate. Wrong here means unusable verification mails.
DB_CONNECTIONsqlitepgsql, mysql, mariadb or sqlsrv, with the matching DB_HOST/DB_PORT/DB_DATABASE/DB_USERNAME/DB_PASSWORDSQLite is a single file with one writer. The roadmap names Postgres for Phase 0.
SESSION_SECURE_COOKIECommented out → trueStops the session cookie being sent over plain HTTP. Requires TLS to already be in place, or nobody can sign in.
SESSION_ENCRYPTtrue (.env.example default; the local .env has false)Session payloads at rest in the store.
MAIL_MAILERlogsmtp (or a provider driver), with MAIL_HOST/MAIL_PORT/MAIL_USERNAME/MAIL_PASSWORD and a real MAIL_FROM_ADDRESSThe single blocker on can_register. Without it every signup is stranded unverified.
CACHE_STOREfile/database → a shared store such as redisThe 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 + workerKeep database (or move to redis) and add a supervised php artisan queue:workThe driver alone does nothing. The process is the missing half.
SchedulerInstall * * * * * php artisan schedule:run on the hostOtherwise sanctum:prune-expired never runs and nothing says so.
World geo storeProvision per driverconfig/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.

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.

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.

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:

  • testcargo fmt --all --check (Ubuntu only), cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace, and cargo bench --workspace --no-run (Ubuntu only). The OS matrix is conditional: pull requests get ubuntu-latest alone, while pushes to main, 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 one cfg(target_os) site, in ds-platform.
  • features — clippy over the gui and remote feature builds of demostatics, clippy over mock-stream-server --features all, and a gRPC backfill integration test.
  • securitycargo-deny for advisories, licenses and bans.

release.yml triggers on tags matching v* and builds four targets:

TargetRunnerArtifact
x86_64-unknown-linux-gnuubuntu-latest.tar.gz via scripts/package-linux.sh
aarch64-apple-darwinmacos-latest.dmg via scripts/package-macos.sh
x86_64-apple-darwinmacos-latest.dmg via scripts/package-macos.sh
x86_64-pc-windows-msvcwindows-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.

  • Code signing certificates. Status: Not built. scripts/package-macos.sh has a codesign --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.sh says “Codesign + notarize only when configured”, but only the codesign call exists — there is no notarytool submission or stapling step. That has to be written, not just credentialed.
  • Signed auto-update. Status: Planned. ds-updater is not a crate. The crates/ directory holds eighteen ds-* crates and ds-updater is not among them; it appears only in demostatics-pc_application/docs/ARCHITECTURE.md and demostatics-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.

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, so eas build has nothing to read.
  • No development client. package.json scripts stop at expo start, expo export --platform web, typecheck, lint and test.
  • No CI. The repository has no .github/ directory, so the Jest suite and tsc --noEmit run only on a developer’s machine.
  • Configuration is one variable. .env.example documents EXPO_PUBLIC_API_URL and nothing else; the default falls back to plain HTTP on localhost, as noted above.

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.

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.

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.

PrerequisiteStatusNote
Counsel engaged in the intended home jurisdictionNot builtThe 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 linesNot builtAdvisory, 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 entityNot builtEverything 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 accountNot builtThe roadmap names Stripe. Selling from the web is Phase 3.
Public Terms of Service and Privacy URLsNot builtRequired 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 accountsNot builtMulti-week. Start before you need them.
Paid Applications AgreementNot builtSeparate from the developer account, and separately slow.

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.

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.

RequirementStatusWhat is missing
Record retentionNot builtNothing 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 trailNot builtThere 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 functionsNot builtApp\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 factNot builtPoint-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.

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.

Terminal window
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.

FieldHealthy valueWhat a bad value means
world_storetruefalse 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_registertruefalse 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.
queueprocessedrequires_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.