This also means we can clean up the last blob operations that were per cid rather than user+cid 🪓
We should be setting ourselves up to be able to go garbage collect blobs safely so deleting the logo blobs manually won't matter anyway.
The current tranquil database design only allows each blob to be owned by one account. This means that if a second account also has that blob, tranquil skips associated the blob with the account. That works fine a lot of the time, since blobs are looked up by cid and the blob exists. However, it can lead to loss of data under certain scenarios.
One example is where I upload a blob, the blob already exists in my instance so insertion is skipped (postgres requires cid to be unique in blobs, fjall only allows one owner per blob cid), I then decide to migrate off tranquil, the blob does not come with me since it is not mine.
Another example is where an account is deleted. If a blob was uploaded for account a, then account b uploads the same blob tranquil skips storing it since it exists. Then I delete account a, now account b's blob is missing.
I accidentally stumbled upon this when I migrated my account to my own tranuil instance and list blobs now lists 2 fewer blobs than before, two images that had been uploaded by accounts already on the PDS.
ps I found record_blobs a bit confusing, at first it looked like a blob ownership table, but then it turns out to just be used for migrations!
This PR makes the blob primary key be cid+user for postgres, and updates the queries to account for there being multiple "blobs" with the same cid. For queries that just care about the blob existing, it doesn't matter "whose" blob it is, so limit 1.
Most of the work is on the metastore side. Adds ref_count to track how many are referencing the blob since we can't just check for other rows. Instead of storing blobs directly, we now store a per account cid, and the blob reference itself is shared and keyed by cid only. This means some of these operations now require updating two places, so they're done in `batch`es.
With the new layout get_blob_value becomes simpler, all blob data is a single "table" or whatever it's called, so we just grab it using cid instead of looking it up for the user.
Migrates blobs rather than maintaining two different versions of the tables, although it seems like that could be supported.
I removed a test that asserted the old behavior, and added a reasonable (?) set of new tests that assert the new behavior, including a parity test.
This adds security alerts section to the communication settings, with one new item: a toggle for enabling/disabling legacy login alerts. It's enabled by default. Disabling it means that you no longer get emails when logging in through a non-2FA enabled login flow, like createSession.
The reason I tackled this is that I have a bot account that automatically refreshes its session, using username and app password, and I'm close to having 100 of these emails in my inbox. I also tried to just disable TOTP but I wasn't allowed (I fixed that in a different PR).
English and Swedish translations were me, the rest were MyMemory. I imagine someone can improve on them after this is merged!
This removes the `require_legacy_session_mfa` call on the TOTP disable path. It looks at the last session, which isn't really relevant, and we're getting a fresh TOTP from the form anyway, which should be enough proof to allow disabling TOTP.
The way I bumped into this was having a "bot account" log in with app password on some app on a schdule, triggering lots of warning emails. So I tried to disable TOTP to stop the flood and wasn't allowed.
Running tranquil with `just run-dev` using `pds.test` seems to be broken due to a recent change that blocks requests to private ips. Inside the compose network `pds.test` resolves to the traefik container's private IP. So it can't make requests to stuff like `https://pds.test/oauth-client-metadata.json` or the local plc.
Introduces a new flag, default off, that allows connecting to private IPs, set to true for the dev compose.
Bluesky started sending `x-bsky-is-beta-user` for some users on XRPC requests, but tranquil has a strict allowlist of CORS headers. The spec doesn't (?) specify any requirements around CORS headers, so we can avoid trouble when Bluesky make changes by just allowing all headers.
This PR replaces the allowlist with the request mirror behavior, where any headers sent on a CORS request are echoed back. This also matches the reference PDS.
is_valid_uri required a literal "://", but the atproto uri string
format follows RFC 3986's generic URI grammar, which also allows
"scheme:opaque-part" forms with no authority (e.g. urn:isbn:...).
Records using such values were rejected once production lexicons
enable strict validation.
Reported as #130.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1. Set the max connections to 20, I frequently see
db-1 | 2026-08-23 08:57:02.784 UTC [12641] FATAL: sorry, too many clients already
2. Add a wildcard route in Traefik to serve DID documents locally.
3. Expose the PLC port locally so I can point an app at `http://localhost:2582` and be able to do full OAuth flows.
- eliminates panic opportunity on receiving email
- strict enum
- added unit test for ensuring that atmos headers don't leak onto
directmx
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a defaulted to off option to add the `X-Atmos-Category` headers
to emails sent via smarthost, for proper categorization by comail.
Category breakdown is as follows:
verification: EmailVerification, ChannelVerification, ChannelVerified,
MigrationVerification, LegacyLoginAlert, EmailUpdate, PlcOperation,
AccountDeletion
password-reset: PasswordReset, PasskeyRecovery
mfa-otp: TwoFactorCode
bulk: Welcome
untagged: AdminEmail