3. Installation

Pepsi can be installed two ways. Building from source out of the Cargo workspace gives you the binaries and leaves it to you to create the service accounts, set the privileged bits and lay out the runtime directories. The prebuilt Debian package does all of that for you — its postinst creates the accounts, groups, directories, privileged-binary overrides and the database — so only the site-specific bootstrap (editing the config and running pepsi-setup) is left to do.

The notebook below covers each path: pick the tab for the method you are using. Everything after it — provisioning the schema/keys/DNS, running the services, the privilege model and the schema lifecycle — is the same for both.

3.1. Prerequisites

  • PostgreSQL — one database, into which Pepsi installs the single pepsi schema. All components connect to the same database. (The Debian package Depends on postgresql and pulls it in for you.)

  • For real mail flow: public DNS you control for each served domain (to publish DKIM, SPF and MTA-STS records), and the ability to bind the privileged SMTP port 25 (and 465/587 for submission). A development build can run entirely on unprivileged ports and a throwaway database.

3.2. Installing Pepsi

Build the workspace, place the binaries and SQL with make install, and create the system accounts and privileges yourself (the same ones the package would create — see Users, groups and privileges below).

Additional prerequisites

  • A recent Rust toolchain (the workspace uses edition 2024; build with a stable cargo that supports it).

  • The vendored GNU Taler Rust submodule, fetched before the first build:

    git submodule update --init --recursive
    

Building

Pepsi is a Cargo workspace whose root package (pepsi) owns every binary under src/bin/; the pepsi-* member crates are libraries the thin binaries call into. A development build produces one executable per program:

cargo build --workspace

A release build instead links most programs into a single multi-call binary, pepsi, which selects the program to run from the name it is invoked under (argv[0]). This is selected by the unibin Cargo feature (which the make build target enables for you); the default multibin feature keeps the per-program binaries used for development and the test suite:

make build   # == cargo build --release --no-default-features --features unibin

Either way the per-program names are what you run; the release layout simply installs them as symlinks to the one binary (see Installing). Some programs are not folded in and stay standalone; the authoritative list is the Makefile’s STANDALONE_BINARIES. Two reasons put a program there:

  • it carries its own setuid or setgid bit, which one shared binary cannot do per program — the local-delivery, ~/.forward, wallet and smarthost helpers and their calling stages, pepsi-whitelist, and pepsi-stage-encrypt and pepsi-stage-decrypt (both setuid pepsi-crypto, so they can reach the private key material);

  • it is deliberately separable — pepsi-setup, pepsi-config, pepsi-stage-detect-language (its language models are large; that binary is itself multi-call, and a second $PATH symlink, pepsi-detect-language, runs the offline diagnostic tool out of it) and pepsi-telemetry (which ships in its own package and runs on a separate host).

The full set of binaries is:

  • pepsi-ingress — inbound SMTP server

  • pepsi-dispatch — stage dispatcher

  • pepsi-stage-arc, pepsi-stage-srs, pepsi-stage-encrypt, pepsi-stage-decrypt, pepsi-stage-dkim-sign, pepsi-stage-bounce, pepsi-stage-relay-to-internet, pepsi-stage-relay-to-smarthost, pepsi-stage-discard — the stages

  • pepsi-setup — provisioning/bootstrap

  • pepsi-queue — queue inspection/repair

  • pepsi-status — pipeline health summary

  • pepsi-sendmail — local submission (installed as /usr/sbin/sendmail by the Debian package)

The four quality gates the project keeps green are:

RUSTFLAGS="-D warnings" cargo build --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --all -- --check
cargo test --workspace

Installing

A bare build only produces binaries; Pepsi also ships SQL migrations and a sample configuration that must be placed on disk. The Makefile wraps both — run it as root so it can also apply the three privileged-binary bits (it prints the exact chown/chmod to run by hand otherwise):

make install PREFIX=/usr/local SYSCONFDIR=/etc DESTDIR=

Better still, run ./configure first and let it resolve the directory variables into config.mk (which the Makefile reads before its own defaults, so what configure decided wins). Every directory derives from --prefix, so naming a prefix and nothing else relocates the whole installation — binaries, SQL, config.d defaults and message templates — below it:

./configure --prefix=/opt/pepsi     # templates at /opt/pepsi/share/pepsi/templates
make && make install

--sysconfdir is the one exception, because a configuration file has to be where the system looks for it: it is /etc when no --prefix was given (and when --prefix=/usr was, since /usr/etc exists on no system), and $(PREFIX)/etc otherwise. Pass --sysconfdir explicitly to override either way.

This performs the following steps:

install-bin

Builds the release (unified) binary and installs it to $(DESTDIR)$(PREFIX)/libexec/pepsi/pepsi, then creates one $(DESTDIR)$(PREFIX)/bin symlink per folded program name pointing at it (e.g. pepsi-ingress -> ../libexec/pepsi/pepsi). The six standalone programs are installed as their own $(DESTDIR)$(PREFIX)/bin executables. Because each symlink still lives in $(PREFIX)/bin (on $PATH), the runtime DATADIR derivation below is unaffected.

install-data

Copies the SQL files from pepsi-setup/db/ to $(DESTDIR)$(DATADIR)/sql (where DATADIR = $(PREFIX)/share/pepsi). pepsi-setup reads these when installing the schema; the default SQL_DIR is ${DATADIR}/sql.

install-templates

Copies the message templates (<name>.<lang>.body — the anti-spam payment request, the vacation notice, the bounce bodies) to $(DESTDIR)$(TEMPLATEDIR), which defaults to $(DATADIR)/templates. The binaries’ [pepsi] TEMPLATE_DIR default is ${DATADIR}/templates and resolves through the same runtime derivation, so the two ends agree under any prefix without the configuration naming a path.

install-config

Installs pepsi.conf.sample and, only if no live config exists yet, a pepsi.conf under $(DESTDIR)$(SYSCONFDIR)/pepsi/ (an existing config is preserved).

Override PREFIX, SYSCONFDIR and DESTDIR to suit the target system. make uninstall removes the binaries and SQL but leaves the live pepsi.conf in place.

Note

DATADIR must match what the binaries compute at runtime: taler-common derives ${DATADIR} as <install-prefix>/share/pepsi from the binary’s own location. When running from a source checkout (not installed), set [pepsi-postgres] SQL_DIR explicitly so pepsi-setup finds the migrations.

Preparing the system

The Debian package’s postinst does the steps below automatically; on a source install you do them yourself, using the same account, group, mode and directory layout documented under Users, groups and privileges and Directories and secrets below. Create the accounts and groups before make install (so it can apply the privileged bits), and the database roles before pepsi-setup (which installs the schema):

  1. Create the five system accounts (pepsi, pepsi-ingress, pepsi-httpd, pepsi-owner, pepsi-helper-token-refresh), each --system with /usr/sbin/nologin and home /var/pepsi and a private group of the same name, plus the two gating groups pepsi-maildir and pepsi-token:

    for u in pepsi pepsi-ingress pepsi-httpd pepsi-owner \
             pepsi-helper-token-refresh; do
        addgroup --system "$u"
        adduser --system --home /var/pepsi --no-create-home \
                --ingroup "$u" --shell /usr/sbin/nologin \
                --disabled-password "$u"
    done
    addgroup --system pepsi-maildir
    addgroup --system pepsi-token
    
  2. Create the four PostgreSQL login roles and the database (peer auth, as the postgres superuser):

    createuser pepsi-owner pepsi pepsi-ingress pepsi-httpd
    createdb -O pepsi-owner pepsi
    
  3. Run make install as root (above) so the three setuid/setgid bits are applied; otherwise apply them by hand per Privileged binaries below.

  4. Create the runtime directories with the ownership and modes listed under Directories and secrets. pepsi-setup creates /var/pepsi/keys for you; create the smarthost-secret directories only if you configure a smarthost using them.

The package is the much simpler path: it ships the binaries (installed to /usr/bin), the SQL schema, the message templates, the man pages, the GNU info manual and socket-activated systemd units, and its postinst provisions the whole system for you.

Installing the package

Install the .deb with apt so its dependencies (including postgresql) are pulled in:

apt install ./pepsi_*.deb

On configure the postinst automatically:

  • creates the five service accounts and the pepsi-maildir / pepsi-token gating groups (the same identities the source install makes by hand);

  • creates the /var/pepsi runtime directories — keys, tokens, token-refresh, tls, krb5 — and /etc/pepsi/secrets with the ownership and modes documented under Directories and secrets below;

  • applies the three privileged-binary bits via dpkg-statoverride (pepsi-helper-maildir-writer setuid root, the maildir and smarthost relay stages setgid); and

  • best-effort creates the four PostgreSQL login roles and the pepsi database (owned by pepsi-owner) when a local cluster is reachable — printing the roles to create by hand if it is not.

The service is shipped disabled: the package installs but does not start Pepsi, leaving the site-specific bootstrap to you.

Note

pepsi is one of four binary packages built from this source: the language-detection stage and the telemetry collector are split out for size and deployment reasons, and pepsi-httpd-admin — which pepsi Recommends, so apt installs it by default — carries the only mechanism by which the browser console can cause a privileged change on this host. Removing that one package is a supported and meaningful hardening step for a deployment administered from a terminal. Debian packages describes each package and that lever in full.

After the package is installed

The package places the configuration at /etc/pepsi/pepsi.conf. Edit it, then run the bootstrap exactly as in the shared steps below — for example:

# 1. edit /etc/pepsi/pepsi.conf (HOSTNAME, ACCEPTED_DOMAINS, domains, TLS)
# 2. install the schema + DKIM keys and print the DNS records:
pepsi-setup --no-certbot run        # or:  pepsi-setup --wizard
# 3. publish the printed DNS records, then start the pipeline:
systemctl enable --now pepsi.target

Because the units are socket-activated, pepsi.target brings up pepsi-ingress, pepsi-httpd and pepsi-dispatch together; you do not invoke the serve subcommands by hand.

3.3. Migrating from an existing mail server

If this host already runs Postfix, Exim, Sendmail, qmail or Stalwart, you do not have to re-derive its configuration by hand. With no pepsi.conf present, pepsi-setup --wizard detects what is installed and offers to import it: the hostname, accepted domains, smarthost (with the relay credentials read from that server’s own password file), local-delivery method, submission SASL socket, trusted networks, message size limit, queue lifetime and TLS certificate paths all arrive as the interview’s pre-filled answers, which you then confirm or correct one by one.

To see what a migration would produce before running the wizard — nothing is written:

pepsi-setup import

Routing tables are converted too. /etc/aliases, /etc/postfix/virtual, Sendmail’s virtusertable, qmail’s .qmail-* files and their equivalents become a Pepsi alias map beside the configuration (see pepsi-stage-aliases), with bare alias names qualified in the style you choose. Since Pepsi alias keys must carry a domain, postmaster: root becomes postmaster@example.org  root@example.org for each accepted domain by default.

Everything that does not carry over is written to import-report.txt beside the configuration, split into what failed to parse, what Pepsi has no equivalent for (header_checks, an alias delivering to a command), what was approximated (mbox delivery becoming Maildir) and what the importer did not recognise. An import never aborts setup — an unreadable file or an unknown directive produces a report entry, not a failure.

Two things are deliberately not migrated: queued mail (let the old server’s queue drain before switching the MX over) and DKIM keys (pepsi-setup run generates fresh ones and prints the records to publish). If the old MTA still holds port 25 when the wizard runs, the report says so — stop and disable it before starting pepsi-ingress.

See pepsi-setup for the per-server detail, the --alias-style choices and the --expert options that expose the settings the interview does not otherwise ask about.

3.3.1. Mail filters already on the host

Independently of any migration, the wizard scans the host for milter daemonsclamav-milter, rspamd, spamass-milter, milter-greylist, milter-regex, mimedefang, amavisd-milter — and offers each one it finds as a stage. Finding one means three things in turn: the package is installed, a socket can be read from its own configuration (or its unit, or the paths its distribution ships), and a real milter option negotiation completes against that socket. Nothing is offered on the strength of an installed package alone, and the negotiation is also how the generated ALLOW_ACTIONS comes to grant exactly what that filter asks for rather than a guess.

Accepted filters are placed by what they do — policy and greylisting, then virus, then spam, then general-purpose frameworks — after decryption and before the whitelist and paywall gates. Because Pepsi filters after accepting a message, a filter’s REJECT is routed to a generated discard stage rather than bounced, so forged senders get no backscatter; see pepsi-stage-milter. Filters whose job Pepsi already does (opendkim, openarc, opendmarc, SPF daemons, postsrsd) are named and skipped rather than offered.

3.4. Provisioning the database, keys and DNS

With a configuration file in place (see Configuration), run the bootstrap tool once:

pepsi-setup -c /etc/pepsi/pepsi.conf run > pepsi-dns.zone

This validates the whole configuration, installs the pepsi schema, generates per-domain RSA-2048 and Ed25519 DKIM keys under KEY_DIR (only if absent), and prints the DNS records to publish (DKIM public keys, an SPF policy from the configured PUBLIC_IP addresses, and an MTA-STS record). The operation is idempotent and may be re-run after upgrades. run --reset drops and recreates the schema (destroying all queued mail; keys on disk are kept).

Publish the printed DNS records, then verify what is live against what Pepsi expects:

pepsi-setup -c /etc/pepsi/pepsi.conf check

See pepsi-setup for the full provisioning workflow.

3.5. Setting up in a browser

Everything above can also be done from a browser, against the same code. The terminal path is not going away and is still the shortest route on a machine you have a shell on; the browser path exists for the ones you do not, and for operators who would rather see the DNS records as a red/green checklist than as a zone file.

3.5.1. The split, and why

Setup is privileged: it writes /etc/pepsi/pepsi.conf, hands each secrets.d fragment to the single account that reads it, runs certbot, creates database roles and generates key material — all as root. pepsi-httpd, which serves the interface, deliberately drops privileges before it accepts a connection and must never regain them.

So it does not act. It writes an intent row saying what should be true, and a separate root program — pepsi-setup apply, started on demand and gone again when it has nothing to do — decides how, does it, and records what happened. Root is never on the network. Read “The applier’s trust model” in :doc:`programs/pepsi-setup` before enabling this: it states exactly what the applier will and will not do, including the things it deliberately cannot (there is no restart or shutdown task, and install-schema refuses reset).

3.5.2. Enabling it

The Debian packages ship a minimal but complete configuration, an administrative listener on a UNIX socket, and — in pepsi-httpd-admin, which pepsi Recommends and apt therefore installs by default — the applier’s socket unit (see Debian packages). Three steps:

# systemctl enable --now pepsi-httpd.socket pepsi-setup-apply.socket
# pepsi-setup -c /etc/pepsi/pepsi.conf bootstrap

The second prints a one-time token and the curl command that turns it into the first administrator account. On the machine itself you can skip it entirely: a member of the pepsi-admin group (and root) is identified by SO_PEERCRED over /run/pepsi/admin.sock and needs no credential at all.

One more setting is needed before anything privileged can be asked for: [pepsi-admin] CONFIG_DB must name a connection that authenticates as the pepsi-config database role. That is not an oversight to work around — it is the boundary. Only that role may enqueue setup work, and pepsi-httpd’s own account deliberately is not it, so that a compromise of the web tier is not a compromise of the pipeline definition. Until it is set the setup endpoints answer 503 and say so.

3.5.3. Driving it

The interview is data. GET /api/v1/setup/questions returns the ordered steps and, for each question, its shape, default, help text and the condition under which it is asked — the same model pepsi-setup questions prints and the same one the terminal wizard is described by, asserted equal by the test suite. Answers are staged with PUT /api/v1/setup/answers into draft rows that no running process can see, so an interrupted session cannot half-configure anything.

POST /api/v1/setup/tasks then asks for the privileged half — writing the configuration, obtaining certificates, installing the schema, generating keys — and GET /api/v1/setup/tasks/{id}?since=<seq> streams what the applier is doing, line by line.

3.5.4. What will always need a text editor

Some settings are read from the configuration file only, and no interface will change that: [pepsi], [pepsi-postgres], [paths], and the HTTP(S) and ingress listener sections. They have to work before a database connection exists, or they are a security boundary — a database that could move a listening socket or relax the crypto policy would make a database compromise a compromise of the MTA’s own TLS and ports.

The practical consequence, stated plainly: adding a submission port, or moving a listener, is a text-editor-and-restart operation. So is any change the console marks restart rather than hot: there is deliberately no restart button, because a button that restarts a service on request would hand the web tier’s privileges to whoever reaches it.

3.5.5. Scripting it

The same answer model drives an unattended install:

pepsi-setup --wizard --answers answers.json -c /etc/pepsi/pepsi.conf

pepsi-setup questions prints the schema that file is written against. This is not a third implementation of the interview: it runs the interactive one with every prompt answered from the file, so the branches and the per-field validation are the same ones a human sees.

3.6. Running the services

Two long-lived processes need to run continuously:

  • pepsi-ingress serve — accepts mail (typically under a service manager, optionally with systemd socket activation for the privileged ports).

  • pepsi-dispatch serve — runs exactly one instance per system; it is the only process that starts stage programs.

The stage programs themselves are spawned by the dispatcher as persistent PROGRAM worker processes that read message ids on standard input; you do not run them by hand in production (for debugging you can pipe an id to worker, e.g. echo 42 | pepsi-stage-srs -c /etc/pepsi/pepsi.conf worker).

3.7. Users, groups and privileges

Pepsi never runs as root. The long-lived daemons may be started as root so they can bind privileged ports and read root-only TLS keys, but each one drops to its own unprivileged service account before serving a single connection; if the drop cannot be completed the process refuses to start rather than risk running as root (see pepsi-common::privdrop). Local delivery — which must write into arbitrary users’ mailboxes — is the one operation that needs more than the service user can give, and it is confined to a single narrowly-scoped setuid helper rather than handed to the pipeline at large.

Under the shipped systemd units the daemons are never root at all: systemd binds the privileged sockets (socket activation) and starts each process as its service account directly. That leaves the TLS keys, which certbot keeps readable by root only — so pepsi-setup has systemd read those too, writing a LoadCredential= drop-in per unit: systemd opens each certificate and key as root when it starts the unit and hands the service a private copy under $CREDENTIALS_DIRECTORY, which is where the server looks first. Re-run pepsi-setup run whenever you add, move or remove a certificate, so the drop-in is regenerated. See pepsi-httpd.

The browser setup described above is the one path by which something other than an operator at a terminal can cause a root process to run: pepsi-httpd rings a doorbell socket and systemd starts a short-lived root pepsi-setup apply. That path is deliberately the easiest privilege in Pepsi to take away. Its two systemd units are a separate Debian package, pepsi-httpd-admin, whose source-install equivalent is make install INSTALL_ADMIN_UNITS=no; without them nothing drains pepsi.setup_task and no HTTP request can change anything under /etc/pepsi, while pepsi-setup on a terminal keeps working exactly as before. Putting the package back does not retroactively act on what was asked for while it was away: installing it empties that queue before its units are armed. See The split as a hardening lever.

The accounts and groups below are created automatically by the Debian package’s postinst; on a make install you create them yourself (the same names). PostgreSQL access is by peer authentication over the local socket: each service account logs in as a same-named database role, so no passwords are stored anywhere.

3.7.1. Service accounts

These are system accounts (--system, /usr/sbin/nologin, home /var/pepsi), each with a private primary group of the same name. The first three are long-lived daemons that drop to their account after binding; the rest are not daemons in the pipeline.

Account

Runs

Purpose / access

pepsi-ingress

pepsi-ingress

Inbound SMTP server. Socket-activated under systemd (never root, TLS material arrives as a service credential); started by hand it binds 25/465/587 and reads the TLS keys as root, then drops. DB role pepsi-ingress.

pepsi-httpd

pepsi-httpd

HTTP/HTTPS server (MTA-STS policy + /metrics). Socket-activated under systemd, exactly like pepsi-ingress; started by hand it binds 80/443 as root, then drops. DB role pepsi-httpd.

pepsi

pepsi-dispatch and every stage worker it spawns

The pipeline’s working identity. Reads the DKIM keys (via group pepsi) to sign. DB role pepsi (owns the queue rows). This is the account that gains the two delivery group-identities below transiently.

pepsi-owner

pepsi-setup (transiently)

Not a runtime service. Owns the pepsi database, schema, tables and functions. When pepsi-setup runs as root it temporarily assumes this identity (seteuid) so the objects it creates are owned by the peer-authenticated pepsi-owner role, then returns to root for the filesystem work (key generation, ownership fix-ups).

pepsi-helper-token-refresh

pepsi-helper-token-refresh service

Only present when a smarthost uses AUTH = oauth. Refreshes OAuth access tokens and writes the token files the smarthost relay reads. Has no database role.

pepsi-crypto

pepsi-keys (transiently), and the end-to-end crypto stages

Custodian of private end-to-end key material. Owns secrets.d/pepsi-crypto.secret, the key-encryption key that opens pepsi.crypto_identity.private_wrapped, and is the only database role granted that column — every other role, pepsi included, gets a column-level grant that omits it. pepsi-keys assumes this identity for the duration of a database call and gives it straight back. See Key management.

pepsi-whitelist

pepsi-whitelist (transiently)

Account the setuid whitelist CLI assumes so ordinary users can maintain their own sender whitelists. Its database role is granted SELECT/INSERT/DELETE on pepsi.whitelist and nothing else.

The PostgreSQL login roles — pepsi-owner, pepsi, pepsi-ingress, pepsi-httpd, plus the narrow pepsi-whitelist and pepsi-crypto roles — are created by the package postinst (or, on a manual install, by you) before pepsi-setup installs the schema; pepsi-setup then grants each what it needs.

3.7.2. Groups gating shared resources

Two groups exist purely to gate access to privileged binaries and secret files. The pepsi user is deliberately not a permanent member of either: the setgid bit on the relevant stage binary grants the worker the group identity only for the lifetime of that process.

pepsi-maildir

Gates the local-delivery helper. pepsi-helper-maildir-writer is setuid root and mode 4750 (root:pepsi-maildir), so only this group may execute it. pepsi-stage-relay-to-maildir is setgid this group (mode 2755, root:pepsi-maildir); when the pepsi worker execs the stage it gains egid=pepsi-maildir, exactly the right to launch the helper — which then setuids to the recipient and writes ~/Maildir/new/. The helper is the only code that ever touches a third party’s mailbox.

pepsi-token

Gates the secret material the smarthost relay reads: OAuth token files, the mutual-TLS client cert/key, and the Kerberos credential cache. pepsi-stage-relay-to-smarthost is setgid this group (mode 2755, root:pepsi-token); the directories holding those secrets are setgid pepsi-token so files dropped into them inherit the group, and the relay can read them without the pepsi user being a member.

3.7.3. Privileged binaries

These three bits are set by make install when run as root (it prints the exact chown/chmod to run by hand otherwise), and by the Debian package via dpkg-statoverride:

Binary

Mode / owner

Why

pepsi-helper-maildir-writer

4750 root:pepsi-maildir

Setuid root so it can write into any local user’s mailbox; group-locked so only pepsi-maildir may run it.

pepsi-stage-relay-to-maildir

2755 root:pepsi-maildir

Setgid so the pepsi worker may exec the helper above.

pepsi-stage-relay-to-smarthost

2755 root:pepsi-token

Setgid so the pepsi worker may read the pepsi-token secrets.

Every other binary is installed unprivileged.

3.7.4. Directories and secrets

pepsi-setup and the package create the following under /var/pepsi (the service home) and /etc/pepsi. The setgid (2750) directories cause new files to inherit the directory’s group, so token/TLS/Kerberos material is group-readable by the relay without any per-file chgrp.

Path

Owner / mode

Contents

/var/pepsi

pepsi-owner:pepsi

Service home.

/var/pepsi/keys

pepsi-owner:pepsi 2750

Per-domain DKIM private keys: written by pepsi-setup, read by the pepsi dispatcher (group pepsi) to sign.

/var/pepsi/tokens

pepsi-helper-token-refresh:pepsi-token 2750

OAuth access-token files: written by the refresher, read by the smarthost relay via group pepsi-token.

/var/pepsi/token-refresh

pepsi-helper-token-refresh 0700

Private rotated refresh tokens; never group-readable.

/etc/pepsi/secrets

root:pepsi-helper-token-refresh 0750

The @inline-secret@ token-refresh credentials; only the refresher may read them.

/var/pepsi/tls

root:pepsi-token 2750

Mutual-TLS client cert + key for the smarthost relay (TLS_CLIENT_CERT / TLS_CLIENT_KEY).

/var/pepsi/krb5

root:pepsi-token 2750

Kerberos credential cache for AUTH = gssapi smarthosts (point KRB5CCNAME at a FILE: cache here; an external keytab refresher populates it).

The /var/pepsi/tokens, /var/pepsi/tls and /var/pepsi/krb5 directories (and the pepsi-helper-token-refresh account) only matter when you configure a smarthost with the corresponding AUTH mechanism; a default direct-to-MX or local-delivery deployment uses none of them.

3.8. Database schema lifecycle

The schema is a series of numbered patch files under the pepsi SQL prefix (in pepsi-setup/db/):

  • pepsi-NNNN.sql — the numbered patches. pepsi-setup applies them in order (pepsi-0001.sql, pepsi-0002.sql, …) until one is missing.

  • procedures.sql — re-creatable stored functions (CREATE OR REPLACE), re-applied on every run.

  • drop.sqlDROP SCHEMA pepsi CASCADE, used only by run --reset.

  • versioning.sql — the patch-tracking machinery.

Applied patches are recorded (by their register_patch name) so re-runs are idempotent.

The patch number is not bumped for every schema change. A new pepsi-NNNN.sql is started only for the first schema change made after a release — that is, after a Git tag of the form v$MAJ.$MIN.$REV. Between releases the current top patch file is edited in place: no released deployment carries that not-yet-released schema, so its statements can be added to or rewritten freely. Once a release is tagged that file is frozen, and the next change to touch the schema becomes pepsi-(NNNN+1).sql, whose register_patch declares the previous patch as its dependency — so databases deployed against the released schema migrate forward incrementally (with ALTER), while a fresh install still applies every patch in order.

There has been no release yet, so the entire schema currently lives in the single pepsi-0001.sql (CREATE-only, no ALTER/DROP). The first schema change after the first v… tag will become pepsi-0002.sql (keep the filename and its register_patch name in lockstep).