An email platform holds other people's secrets. Customers hand it SMTP credentials, API keys that can send on their behalf, and the DKIM private keys that let it sign mail as their domain. If any of those leak, the damage is not to InkPigeon but to the customer's domain and reputation.
So InkPigeon's standards file has a short section on secrets, and the rules in it are not negotiable in code review. They are short enough to list.
Hashing and encryption, by kind of secret
Not every secret needs the same treatment, and using the wrong primitive is a common mistake.
Passwords and SMTP credentials are hashed with argon2id. These are things a human or a mail client presents on every use, and the threat is an attacker with the database trying to recover them. Argon2id is memory-hard, which makes that expensive, and it is the current recommendation for exactly this job.
API keys are hashed with SHA-256. This looks like a downgrade, but it is the right tool: API keys are long random strings generated by the platform, not chosen by a human, so there is nothing to brute-force. What matters is that the database holds only the hash and that lookup is fast. A memory-hard hash on every API request would be waste.
DKIM private keys are encrypted with AES-GCM. These cannot be hashed, because the platform has to use them to sign mail. So they are stored encrypted with a key that lives outside the database, and decrypted in memory when a message is signed. Authenticated encryption, so a tampered ciphertext is detected rather than producing garbage signatures.
The customer sees an API key or SMTP password exactly once, when it is created.
What never goes into logs
The logging rule is phrased as a prohibition: never log secrets, message bodies or full email addresses at INFO level.
Secrets are obvious. Message bodies are customer content, and a log aggregator is not a place that content should ever reach. Full email addresses are personal data under GDPR, and a log line that says delivered to alice@example.com is a record of a person that will outlive any retention policy if it is left to the logging stack.
So INFO logs carry message ids, organisation ids and outcomes. The address is in the database row, where access is controlled and retention is enforced, not in a text file on a server.
Configuration changes are three changes
Every environment variable the application reads is declared in core/config.py with its type and default. Adding one is a rule with three parts: the config class, the backend/.env.example file, and the spec document that lists all variables. All three in the same commit.
The reason is that a configuration value which exists in code but not in the example file is a deploy that fails in a way nobody can diagnose at midnight. And one that exists in both but not in the spec is a value nobody knows the meaning of six months later. The spec is the source of truth for the platform; config is part of it.
Migrations are append-only
Schema changes go through Alembic, and the rule is never edit an applied migration. If a migration is wrong, the fix is a new migration. The test suite includes a migration test that keeps the chain valid.
This is a secrets rule only indirectly, but it belongs in the same list: a database whose history cannot be trusted is a database whose access controls cannot be reasoned about. Row-level policies, roles and grants all live in migrations, and rewriting one after the fact is how a production database ends up differing from what the repository says it should be.
Why rules and not habits
Everything above is a habit a careful engineer has anyway. I wrote them down for two reasons.
The first is that InkPigeon is built partly in AI-assisted coding sessions, and an assistant does not have habits. It has instructions. The standards file is read at the start of every session, and a rule that is in the file gets followed; a rule that is in my head gets followed only when I notice.
The second is that habits erode under time pressure. A written rule is something a reviewer can point at without it being a personal judgement. "The standards say argon2id" ends a discussion that "I think you should use argon2id" starts.
Takeaway
Match the primitive to the secret: memory-hard hashing for human-chosen secrets, fast hashing for machine-generated ones, authenticated encryption for the ones you have to read back. Keep personal data out of logs by rule, not by vigilance. And write all of it down, because the person reading the file may not be you, and may not be a person.