Installation
Requirements
- PHP 8.2+ with extensions
pdo,mbstring,intl(seecomposer.json) - MySQL 5.7+ or MariaDB 10.0+ — InnoDB FULLTEXT is used for duplicate-idea detection; a PHP fallback without FULLTEXT is used automatically if unavailable
- SMTP access (magic-link sign-in has no password fallback — mail delivery is required)
- A webserver that can point its document root at
public/, or serve the whole tree with Apachemod_rewrite/.htaccesssupport (the bundled.htaccessblocks access to everything outsidepublic/if the document root can’t be reconfigured) - No Docker, no SSH requirement, no
composer installon the server
1. Build locally
Dependencies are installed locally and the built vendor/ directory is uploaded —
never run composer install on the shared-hosting server itself:
composer install --no-dev --optimize-autoloader
If you also build the bundled React SPA yourself (see development.md):
pnpm --filter votepit-app run build # → app/dist/, copy into your deploy artifact
2. Create the configuration
cp config/config.example.php config/config.php
Fill in at minimum: db.* (MySQL/MariaDB access), smtp.* (magic-link delivery),
app_url (public base URL, no trailing slash), admin_emails (who becomes admin on first
login), and two secrets:
php -r "echo bin2hex(random_bytes(32));" # app_key
php -r "echo bin2hex(random_bytes(32));" # identity_server_key (run again — separate key)
app_key signs sessions and magic links. identity_server_key is the HMAC key used to
pseudonymize email addresses (see architecture.md
and ADR 0002) — deliberately a separate secret so the two can be rotated independently.
Neither may ever be empty in production; leaving them blank is a misconfiguration, not a
supported “disabled” state. See configuration.md for every other key.
config/config.php is gitignored — never commit it, it holds live secrets.
3. Create the database schema
db/schema.sql alone is not sufficient — it is only the pre-tenancy baseline. Use the
versioned migration runner instead, which applies the baseline and every schema change on
top of it in order:
php bin/migrate.php --dry-run # review what would run
php bin/migrate.php # prompts for a backup confirmation, then applies
Against a brand-new, empty database this applies 0000_baseline.sql (the schema also
captured in db/schema.sql) followed by every NNNN_* migration under migrations/ —
end result: the full current schema, including account/tenancy tables, invites, API
tokens, blocked users, etc. See operations.md for the runner’s
mechanics and safety guarantees.
4. Point the web root at public/
Configure your webserver’s document root to public/. If your hosting doesn’t let you
choose a document root below the repo root, the bundled public/.htaccess +
.htaccess at the repo root block direct access to everything outside public/ instead
(source, config, migrations, vendor) — verify this works for your host before going live,
since .htaccess behavior depends on the webserver having AllowOverride enabled for the
relevant directives.
5. Deploy (shared hosting)
Upload the built folder — public/, src/, vendor/, config/config.php,
migrations/ — over FTPES (or your host’s transfer method) into the docroot. There is no
CI/CD pipeline bundled with this package; deploys are a file sync of a locally-built
artifact.
Upgrading an existing installation
- Back up first — using your own
mysqldump/hosting-provider backup tooling (seeoperations.md— this package ships no backup tooling of its own). Every schema migration requires a fresh backup immediately before running it. - Pull/upload the new code (
src/,public/, updatedvendor/ifcomposer.lockchanged, updated SPAdist/if the frontend changed). - Run
php bin/migrate.php --dry-runto see what’s pending, thenphp bin/migrate.phpto apply. Migrations are forward-only — there is no automated rollback; restore from the pre-migration backup if something goes wrong. - Never hand-edit
0000_baseline.sqlor already-applied migration files — every schema change is a new, additively-numbered migration file.
Product telemetry
This installation sends aggregate, anonymous, cookieless usage signals (e.g. “a board was
created”) to Votepit’s own Matomo instance, to help prioritize development — no page content,
board/account slugs, IPs (anonymized before storage), or other PII is ever included. This is
on by default and can be turned off at any time under Account → Product telemetry
(a plain toggle, no re-install/config-file change needed). If your own privacy policy needs
to reflect this, see Votepit\Telemetry\CommunityTelemetry for exactly what is sent.
If you additionally configure your own matomo_url/matomo_site_id (see
configuration.md) or point the product telemetry at a
different host, review public/.htaccess’s Content-Security-Policy — it currently only
allow-lists matomo.silvio-und-maik.de.
Multiple boards
One installation serves many boards, addressed by path (/mobile-app, /website, …).
Boards are created in the admin area; branding (name, slug, accent color, intro) is
configurable per board. This is not multi-tenancy — self-host installs run as a single
seeded account, and all boards belong to it (see
architecture.md).