ask me anything: private data on pollen.place

pollen just got an AMA feature, and it works differently from everything else on the site. everything else — posts, reblogs, follows, reactions — is written directly to your PDS. public data, owned by you, synced across the network. the AMA post itself works this way too: place.pollen.post.ama is a record on your PDS like any other post.

but the questions people ask? those can't be public. that's the whole point — you ask something anonymously, the AMA owner decides whether to answer, and only then does it become public. atproto doesn't really have a concept of "private data someone else can read." your PDS stores your data, publicly.

the gap

so we have a problem: where do pending questions live?

on atproto, data flows like this: you write to your PDS, jetstream broadcasts the event, other apps index it. but a pending anonymous question doesn't belong in anyone's PDS. the asker might not even have an account. and even if they do, putting "anonymous question to user X" in their public repo kind of defeats the purpose.

this is the first feature on pollen where the canonical data isn't on the PDS.

sqlite as a staging area

questions land in pollen's sqlite database (turso, technically — sqlite at the edge). the schema is minimal:

CREATE TABLE ama_questions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ama_uri TEXT NOT NULL, -- which AMA this is for
ama_owner_did TEXT NOT NULL, -- who gets to see it
question TEXT NOT NULL,
asker_did TEXT, -- null if anonymous
asker_ip_hash TEXT NOT NULL, -- for rate limiting, daily-rotating salt
status TEXT NOT NULL DEFAULT 'pending',
answer_uri TEXT, -- set when answered
created_at INTEGER NOT NULL,
answered_at INTEGER
);

questions come in as pending. the AMA owner can see them in their inbox and either answer or dismiss. dismissed questions just get a status change and eventually age out.

the important part: this data is not promised to stick around. pollen's sqlite database is a cache. it could be rebuilt from the network at any time. for everything else on the site — posts, follows, reactions — that's fine, because the PDS is the source of truth and we can re-sync from jetstream. but pending questions have no other home. if the database goes away, unanswered questions go with it.

this is a trade-off i'm okay with. pending questions are transient by nature. they exist in a liminal state between "someone thought of something" and "this is now a public conversation." losing a few in a database rebuild is sad but not catastrophic.

the promotion

when the AMA owner writes an answer, that's when we graduate to the normal flow. the answer gets written to the PDS as a place.pollen.post.answer record:

{
amaUri: "at://did:plc:xyz/place.pollen.post.ama/abc123",
question: "what's your favorite ice cream flavor?",
answer: "van leeuwen honeycomb, it's not even close",
askerDid: null, // stays anonymous
createdAt: "2026-04-12T..."
}

the question text gets embedded in the answer record. now it's public, permanent, on the PDS, synced across the network. the original pending question row gets marked status: 'answered' with a pointer to the answer URI.

so the lifecycle is: private question in sqlite → owner reviews → public answer on the PDS. the database is a staging area, and the PDS is where things go when they're ready to be real.

rate limiting without accounts

since anonymous users don't have a DID, we rate limit by IP hash — sha-256 with a daily-rotating salt. the salt resets on server restart too, so we genuinely cannot trace questions back to IPs after the fact. the rate limit table uses hourly windows:

CREATE TABLE ama_rate_limits (
ip_hash TEXT NOT NULL,
ama_uri TEXT NOT NULL,
window_start INTEGER NOT NULL,
count INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (ip_hash, ama_uri, window_start)
);

5 questions per AMA per hour per IP. and cloudflare turnstile on the frontend for bot prevention (invisible mode, theoretically — i had a fun afternoon debugging that).

the protocol is catching up

worth noting: daniel holmgren has been writing about permissioned data for atproto — "permission spaces" that act as authorization and sync boundaries for non-public records. the idea is a shared social context with a member list, access control, and short-lived credentials for services.

that's exactly the gap pollen's AMA feature falls into. pending questions are private data that one specific person (the AMA owner) needs to read. right now i'm solving that with "put it in sqlite and hope for the best." permission spaces would let that data live on the protocol — owned, synced, portable, with real access control instead of app-level trust.

it's early (the post calls it "a rough proposal"), but the direction is clear: atproto wants to support private and semi-private data natively. when that ships, the sqlite staging area could become a proper protocol-level inbox. until then this works!

links

pollen

atproto repo spec - how PDS data works