Please help me install and set up "Terracotta" from veered.org on my own computer or server. What it is: Self-hosted community phone and business directory. Project page: https://veered.org/community-directory/ Source code download (zip): https://veered.org/community-directory/terracotta-community-directory.zip How I'd like you to help: 1. First ask me what you need to know: my operating system and version, my desktop environment if it matters, whether I'm comfortable with the terminal, and any accounts or hardware this project needs. 2. Then take me through it one step at a time. For each command, say briefly what it does, and wait for me to paste back the result before moving on. 3. Before any step that changes system settings, uses sudo, or edits an existing configuration file, tell me, and have me make a backup first. 4. If something fails, help me work out why. If the instructions below don't fit my system, say so rather than guessing. 5. At the end, show me how to check that it works, and how to uninstall it. The project is shared as-is by its author, with no warranty. If we find a problem in the project itself, please help me write a short, clear note I can send to support@veered.org. Here is the project's documentation (README files from the download): ===== README.md ===== # Self-Hosted Community Phone and Business Directory (Terracotta) *Why Terracotta? The red clay of the tile roofs on a neighborhood's homes.* **Version 1.0.** A phone and business directory for a neighbourhood, congregation, school or any other community, that lives on your own website. People add themselves; you approve each one with a click; every listing's owner gets a private link to keep it up to date, and anything nobody has confirmed for a year quietly drops off the list. ![The directory as visitors see it](docs/screenshots/directory.jpg) Nobody needs an account or a password — not even you. Visitors search as they type, and can filter to businesses or to people. ![Adding a listing](docs/screenshots/add-a-listing.jpg) Somebody adding themselves fills in this form, confirms their email address, and then waits for you to approve it. You get an email with an Approve and a Reject button. ## Please test before relying on it This is shared as-is, with no warranty. It works on my own computers, but your system, settings and software versions may differ, so please try it in a safe setting first. If something doesn't work, you can ask Claude (or another AI coding assistant) to look into it, and I'd appreciate hearing what you found and how you fixed it. You are also welcome to just let me know at support@veered.org, and I'll look into it. ## Features - **Business and personal listings.** Businesses have a name, category, description and website; personal entries are phone book lines. Each listing can show a work and a home phone and an optional address. - **Live search and filters.** All / Businesses / People tabs, a category filter and instant client-side search, with a server-side fallback when JavaScript is off. - **Automatic categories.** Categories are inferred from what people type, using an editable keyword map (`$CATEGORY_RULES`). Typed categories snap to a known one when possible; otherwise the person's own wording is kept. - **Self-service with moderation.** Submit, confirm email, moderator review, published. The moderator receives a review link by email; approving or rejecting takes one click. - **Owner edit links.** Every approved owner receives a private, unguessable link to update, reconfirm or delete their listing. No login needed. - **Annual reconfirm-or-delist.** A year after the last confirmation the owner is reminded, reminded again two weeks later, and after a 30-day grace period the listing is hidden (the name and phone remain, marked "last updated more than one year ago"). Any reconfirmation restores it. All intervals are configurable. - **Data minimisation.** Unconfirmed submissions and rejected entries are purged automatically; owner deletion removes the record entirely; expired listings can be purged after a configurable period. - **Right-to-left support.** Set `SITE_LANG` (e.g. `he`, `ar`) and the page direction follows, or force it with `SITE_DIR`. Layout uses logical CSS properties; names and phone numbers are isolated for correct bidirectional display. Email subjects and sender names are MIME-encoded, so non-Latin site names work in mail too. - **Mail your way.** PHP `mail()` by default, a built-in SMTP client (STARTTLS or implicit TLS, AUTH LOGIN), or a log file for development. ## Information for nerds: what you need, how to install it, and every setting An ordinary website with PHP — which is what nearly every web host sells, including the cheapest shared hosting. No database to set up, no monthly service to sign up for. *In detail, for anyone who wants to check:* PHP 7.4 or newer (8.x recommended) with `pdo_sqlite`; `mbstring` recommended but optional; SQLite 3.27+ for consistent online backups; Apache with `.htaccess`, nginx (see `docs/nginx.conf`), or any server that can deny access to the files listed further down. ### Putting it on your website **It works as soon as you upload it.** There is nothing to build, no accounts to open and no software to install: almost every web host already runs PHP, which is all this needs. 1. **Upload the folder** to your website, wherever you want the directory to live — for example into a folder called `directory`, so it appears at `https://example.org/directory/`. Your host's file manager, or any FTP program, will do it. 2. **Open that address in a browser.** The directory is already running. It creates its own database on that first visit, and it will tell you, in a red box at the top of the page, the one thing it still needs: who approves new listings. 3. **Tell it who you are.** Rename `config.php.example` to `config.php` (your host's file manager can rename a file) and, inside it, put your own email address in the `MODERATOR_EMAIL` line and your community's name in the `SITE_NAME` line. Save it. The red box disappears, and approval emails now come to you. 4. **Add a listing yourself, as a test**, and follow it through: you will get an email asking you to confirm the address, then one with Approve and Reject buttons, and finally a private link for changing the listing later. That is the whole setup. Everything else on this page is optional. **If step 2 says it cannot write to `data/`:** your host has the folder locked. In the file manager, set the permissions of the `data` folder to 755 (or 775), or see "Shared hosting" below. ### Files that must never be served | Path | Why | |---|---| | `config.php`, `config.php.example` | settings, possibly SMTP credentials | | `data/` | the SQLite database, its backup, the development mail log | | `lib.php`, `mailer.php`, `form.php`, `header.php`, `footer.php` | include-only | | `cron.php` | command-line only (it also refuses web requests itself) | | `docs/`, `*.md`, dotfiles | not part of the site | The shipped `.htaccess` and `data/.htaccess` enforce this on Apache 2.4. For nginx, use the snippet in `docs/nginx.conf`. Best of all, move `DB_FILE` outside the web root. After installing, check that `https://example.org/directory/data/directory.sqlite` and `https://example.org/directory/config.php` return 403 or 404. ### The daily job With `WEB_CRON` enabled (the default), the first page view each day runs the maintenance job: reminders, expiry, purges and the backup. On a quiet site that can be late, so a real cron entry is more reliable: ```cron 15 3 * * * php /path/to/directory/cron.php ``` `php cron.php --force` runs it again the same day. `php cron.php --today=2027-10-01` simulates a date, which is useful to test the reminder cycle. ### Every setting All settings live in `config.php`. Optional ones fall back to the defaults shown. | Setting | Default | Purpose | |---|---|---| | `SITE_NAME` | `Community Directory` | Name shown on the web pages | | `MAIL_NAME` | `SITE_NAME` | Name used in email subjects and bodies | | `SITE_URL` | | Public base URL, no trailing slash. Used in emailed links and the cookie path | | `SITE_LANG` | `en` | Page language | | `SITE_DIR` | `''` | `ltr`, `rtl`, or empty to derive from `SITE_LANG` | | `TIMEZONE` | `UTC` | Used for all dates | | `PHONE_FORMAT` | `none` | `none`, `nanp` (US/Canada) or `il` (Israel) | | `MODERATOR_EMAIL` | | Receives review links | | `MAIL_FROM`, `MAIL_FROM_NAME` | | Sender of all mail | | `MAIL_REPLY_TO` | `MAIL_FROM` | Reply-To header | | `MAIL_TRANSPORT` | `mail` | `mail`, `smtp` or `log` | | `MAIL_SET_ENVELOPE_SENDER` | `true` | `mail()` only: pass `-f MAIL_FROM` | | `SMTP_HOST`, `SMTP_PORT` | | SMTP server | | `SMTP_SECURE` | `tls` | `tls` (STARTTLS, usually port 587), `ssl` (implicit TLS, 465) or `''` | | `SMTP_USER`, `SMTP_PASS` | `''` | AUTH LOGIN credentials; never sent without TLS | | `SMTP_TIMEOUT` | `15` | Seconds | | `MAIL_LOG_FILE` | `data/mail.log` | `log` transport output | | `DB_FILE` | `data/directory.sqlite` | Database path | | `SQLITE_JOURNAL_MODE` | `DELETE` | `DELETE`, `WAL`, `TRUNCATE`, `PERSIST` or `MEMORY` | | `DAILY_BACKUP` | `true` | Refresh `.bak` daily | | `CONFIRM_INTERVAL_DAYS` | `365` | Days until the first reconfirmation reminder | | `REMINDER_GAP_DAYS` | `14` | Days between first and second reminder | | `GRACE_DAYS` | `30` | Days after the first reminder until the listing expires | | `PENDING_PURGE_DAYS` | `14` | Delete submissions whose email was never confirmed | | `REJECTED_PURGE_DAYS` | `30` | Delete rejected submissions | | `EXPIRED_PURGE_DAYS` | `0` | Delete expired listings after this long; `0` keeps them | | `WEB_CRON` | `true` | Run the daily job from page views | | `SUBMIT_LIMIT_PER_HOUR` | `5` | New submissions per client IP per hour; `0` disables | | `BLURB_MAX` | `600` | Maximum description length | | `$CATEGORY_RULES` | see file | Ordered `category => [keywords]` map; first match wins | | `$CATEGORY_EXTRA_KEYWORDS` | example set | Community-specific keywords merged into the map | The shipped `$CATEGORY_EXTRA_KEYWORDS` is an example for a Jewish community (it adds transliterated Hebrew and Yiddish trade terms and a Judaica category). Replace it with your own community's vocabulary, or delete it. ### Mail delivery - **`mail`** works on most shared hosts. Keep `MAIL_SET_ENVELOPE_SENDER` on so the envelope sender matches `MAIL_FROM`; otherwise many hosts send with the account's system address and receivers may file the mail as spam. The `MAIL_FROM` domain should publish SPF (and ideally DKIM and DMARC) records that authorise your server. - **`smtp`** sends through a mailbox or relay provider. Hosts often sign DKIM only for authenticated SMTP, so this usually gives the best deliverability. The client verifies the server's TLS certificate and refuses to send credentials over an unencrypted connection. - **`log`** appends each message to `MAIL_LOG_FILE` and sends nothing. Use it only for development; the log contains personal data and edit links. Mail errors are written to the PHP error log and never shown to visitors. ### When the host will not let it write On many shared hosts PHP runs as a common web user (often `nobody` or `www-data`), not as your account. That user must be able to **write the database file**, and with the default journal mode also **create files in its directory** (SQLite writes a temporary `-journal`, or `-wal`/`-shm` files in WAL mode, next to the database). Pick one: 1. **Preferred:** make only the data directory writable by the web user, e.g. through your host's permission tool or a group (`chgrp www-data data && chmod 2770 data`). 2. If the only option is making a directory world-writable, do not do that. Instead pre-create the files and make just those writable, and set `SQLITE_JOURNAL_MODE` to `MEMORY` so SQLite never needs to create a journal file: ```sh touch data/directory.sqlite data/directory.sqlite.bak chmod 666 data/directory.sqlite data/directory.sqlite.bak ``` `MEMORY` keeps the rollback journal in RAM. It is safe against concurrent requests but a crash in the middle of a write can corrupt the database, so keep the daily backup enabled and copy it off the server from time to time. Files with mode 666 on a shared server may also be readable by other accounts' scripts; prefer option 1 wherever the host allows it. Symptoms of this problem are "attempt to write a readonly database" or "unable to open database file" in the PHP error log. ### Security - All SQL uses prepared statements; all output is escaped with `htmlspecialchars`. - Edit, confirmation and moderation links carry 160-bit random tokens. Confirmation and moderation actions need a button press (POST), so mail scanners that pre-fetch links cannot confirm, approve or reject anything. Moderation tokens are cleared once used. - Forms are protected against CSRF with a SameSite=Strict, HttpOnly double-submit cookie. - Responses send a Content-Security-Policy with per-request script nonces, `X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff` and `Referrer-Policy: no-referrer`, so private links do not leak through the Referer header. - Email headers are built from single-line, MIME-encoded values, preventing header injection. - Submissions are rate-limited per IP; IPs are stored only as salted hashes for an hour. A hidden honeypot field silently discards simple bots. - Anyone holding an edit link can change that listing. Owners should treat it like a password; they can delete the listing at any time. ### Privacy This software publishes names, phone numbers and optionally addresses. **The operator of each installation is responsible** for obtaining consent, publishing a privacy notice, responding to access and deletion requests, and complying with the data protection law that applies to them (for example the GDPR). The software is designed to help: every listing is added by the person it describes and confirmed through their own email address; email addresses are never shown publicly; owners can edit or delete their listing without contacting anyone; unconfirmed and rejected submissions are purged automatically; and the annual reconfirm-or-delist cycle keeps stale personal data from lingering. Consider setting `EXPIRED_PURGE_DAYS` so that expired listings are eventually deleted as well. ## License MIT. See [LICENSE](LICENSE). ----- For Claude Code users ----- Download https://veered.org/community-directory/terracotta-community-directory.zip, unzip it into a new folder, read every README.md in it, and help me install and set up "Terracotta" (Self-hosted community phone and business directory) on this machine. Check the requirements against this system first and tell me what is missing. Ask me before running anything with sudo, installing packages, or changing existing configuration files, and back those files up first. Finish by showing me how to test it and how to uninstall it.