Terracottav1.0

Self-Hosted Community Phone and Business Directory

Why Terracotta? The red clay of the tile roofs on a neighborhood's homes.

✨ Install with Claude's help

Not sure how to install this? Let an AI assistant walk you through it, step by step, on your own system.

  1. Copy the instructions below.
  2. Open Claude (a free account works) and paste them in.
  3. Answer its questions and follow along, doing the typing yourself.

Claude in a web browser cannot reach your computer: it reads the instructions, asks about your system, and tells you what to type and what each command does, one step at a time. You stay in control of everything that actually runs.

Using Claude Code? That is the version that runs in a terminal on your own machine, so it can do the downloading and installing itself, asking before each step. Paste this into it instead:

Open in Claude Code ↗needs the Claude Desktop app

Self-Hosted Community Phone and Business Directory (Terracotta)

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

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

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

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

PathWhy
config.php, config.php.examplesettings, possibly SMTP credentials
data/the SQLite database, its backup, the development mail log
lib.php, mailer.php, form.php, header.php, footer.phpinclude-only
cron.phpcommand-line only (it also refuses web requests itself)
docs/, *.md, dotfilesnot 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:

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.

SettingDefaultPurpose
SITE_NAMECommunity DirectoryName shown on the web pages
MAIL_NAMESITE_NAMEName used in email subjects and bodies
SITE_URLPublic base URL, no trailing slash. Used in emailed links and the cookie path
SITE_LANGenPage language
SITE_DIR''ltr, rtl, or empty to derive from SITE_LANG
TIMEZONEUTCUsed for all dates
PHONE_FORMATnonenone, nanp (US/Canada) or il (Israel)
MODERATOR_EMAILReceives review links
MAIL_FROM, MAIL_FROM_NAMESender of all mail
MAIL_REPLY_TOMAIL_FROMReply-To header
MAIL_TRANSPORTmailmail, smtp or log
MAIL_SET_ENVELOPE_SENDERtruemail() only: pass -f MAIL_FROM
SMTP_HOST, SMTP_PORTSMTP server
SMTP_SECUREtlstls (STARTTLS, usually port 587), ssl (implicit TLS, 465) or ''
SMTP_USER, SMTP_PASS''AUTH LOGIN credentials; never sent without TLS
SMTP_TIMEOUT15Seconds
MAIL_LOG_FILEdata/mail.loglog transport output
DB_FILEdata/directory.sqliteDatabase path
SQLITE_JOURNAL_MODEDELETEDELETE, WAL, TRUNCATE, PERSIST or MEMORY
DAILY_BACKUPtrueRefresh <DB_FILE>.bak daily
CONFIRM_INTERVAL_DAYS365Days until the first reconfirmation reminder
REMINDER_GAP_DAYS14Days between first and second reminder
GRACE_DAYS30Days after the first reminder until the listing expires
PENDING_PURGE_DAYS14Delete submissions whose email was never confirmed
REJECTED_PURGE_DAYS30Delete rejected submissions
EXPIRED_PURGE_DAYS0Delete expired listings after this long; 0 keeps them
WEB_CRONtrueRun the daily job from page views
SUBMIT_LIMIT_PER_HOUR5New submissions per client IP per hour; 0 disables
BLURB_MAX600Maximum description length
$CATEGORY_RULESsee fileOrdered category => [keywords] map; first match wins
$CATEGORY_EXTRA_KEYWORDSexample setCommunity-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.

Information for nerds: the source code

Everything this project is made of, as committed: the code, its README files and its license. No build step or account needed to read it.

⬇ Download source (.zip)

terracotta-community-directory.zip · 235 KB · released under the license stated on this page.

Please test before relying on these tools. They are shared as-is, with no warranty. They work on my own computers, but your system, settings and software versions may differ, so please try them 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.