Spaces - unauthorized write notifications

As space membership controls whose PDS should be read from, rather than who can write, I wanted to discuss how spaces should handle write notifications from non-members.

The most useful situation I can see for keeping writes for non-members is for spaces that don’t exist yet. For example:

  • the invite space for opensocial.communities
  • inbox spaces (either generic, or app specific)
  • analytics space for apps to provide metrics to creators
    There are also some less common use cases (e.g, private space becoming public) that may also benefit from this.

In the spaces alpha (been having some good fun playing around with those) the space host will silently drop all notifications it receives from identities that aren’t currently approved. If a space is later created (or changes to public), the PDS writer set does not get updated. The records written previously are for all intents and purposes ignored for subsequent reads, until that identity writes (triggering a new notification) again to the space.

For something like the new community invites being discussed, this effectively means you can’t send an invite until that user has first used an app which will create this space for them. If I want to invite my friends to their first open social community, that’s not a great flow. If this happens for other apps and games I want to send ATproto messages/invites within, this problem grows.

I see a few options - none of which I love:

  1. We decide this isn’t a big enough problem, and leave it as is.

  2. The spaces could instead hold “ineligible” write notifications, and process them in the future if that changes, removing this race condition for us. This does however open a bit of a spam vector, which I don’t like.

  3. Apps could handle things like invites outside of ATproto spaces; either in a bespoke way, or over the public protocol.

  4. We could have a generic inbox space that could be appropriate for many use cases. This reduces the impact, as either it is more likely the user will have already created the space, or PDS hosts may auto-create it for the user.

I’m not sold on any of these options, and would love to hear others opinions

3 Likes

I think this has come up in the Working Groups > WG Community Standards as well.

For the invite use-case in particular, I don’t think any information should be (attempted to be) initially sent to the invitee on protocol at all, unless you commit to heavy anti-spam moderation (at which point you probably should take it off-protocol anyway, or you still risk storage wasting attacks against your users’ PDSs).

Small sketch of an alternative that doesn’t require data to be stored (only transmitted) off-protocol:

The inviter can simply pre-authorise the invitee to the space (or a staging space owned by the same authority, if interactive joining with notification to other members is required before they can read actual content) and then send them an “invite link” (plain web link) that points to a record in an “invite space” that the inviter creates. This in turn can then point to the actual space (or staging space). If a staging space exists, the invitee will have to write their join intent into that, which nicely authenticates the (in-use, at least) join notification to all members.
The invite space can be publicly-readable to display information without login, but that should not be emulated with a public record since you almost certainly still don’t want these to be enumerated.

With that approach, you then have the classic invite experience for either public Telegram channels or community discords (i.e. where messages can be previewed) or, with staging channel, private discord or (iirc) Telegram or Whatsapp groups where membership is more explicit and a notification is shown whenever someone joined and can now read messages.

(Reading the invite space and writing to the space it points to can be flexibly authorised for multiple invitees for a single invite link with this scheme, I believe. Even with a staging space, you can still pre-authorise reading the content space to allow previews before joining formally, if I’m not mistaken.)

I think a read-only invite space will require the application to act as space host though, since that can’t be modelled by the simplespace space host. You can get away with a symmetric read/write one for limited invitees because the resources wasted by “writing” to it are equally bounded even if all collude, but I’d definitely avoid it for public invite spaces.


The first paragraph is also why I think on-protocol open inboxes that use resources on the recipient’s PDS’s side (rather than e.g. an apps’) are a bad idea in general.


For the analytics use-case, I think it’s generally fine for the analytics recipient to have to know about an app through other means before they agree to accept analytics from it. Metrics-producing apps could advertise their presence and, if it’s for public data, for whom they hold analytics on the public atprotocol.
(They may specify an inbox space or xrpc endpoint that can be nudged to send a write notification to an opting-in account immediately rather than having to wait for the next metrics.)

For apps that generate metrics about non-public atproto space(s/ content), I don’t think it’s feasible to signal for whom they hold data in advance, but they could still signal which kinds (lexicons) of spaces and records they provide data for. Then analytics apps could, on a dedicated page, while having the opportunity to moderate or ideally allowlist, offer a user to opt into (potentially) receiving metrics directly from (specific or all) those providers.

As in spaces the spammers need to write to their own PDS, I’m only receiving (and potentially storing) the write notification. Even that, at least in the spaces alpha, would dedupe so that I only end up storing each "spam” DID once per space they write to, no matter how often or how much they write.

Spam arguments also apply to any publicly writable space, including open groups/communities, which seem to be something we want to support, so we will need to address them head on.

What about if we consider a more focused invite space? For example, lets assume I play chess with my friends over ATproto. We have some generic lexicons (e.g, move.chess.genericlexicon) and there are multiple apps we can choose to use which can support these. With the spaces, now these apps can support private chess games, so I don’t have to broadcast publicly how bad I am at chess. And my friends and I can play these games over the protocol without even caring which app each other is using. This feels very aligned with ATproto principals so far.

Now for these private games, I think the best pattern is to have a new space for each game (gives the best flexibility in allowing other viewers). But this next part still holds in different ways if we use a different model, e.g, a single space for all of my games with another specific player. Now as I want to improve at chess (or I just like boosting my friends egos) so over time I want to play many games with several different friends. To do so, I’ll need to somehow let them know about the new space that’s been created for this game. As we don’t necessarily use the same app, we can’t have an app specific invite process. If I suggest to them I have to email them an invite code every time we want to start a new game, they’ll rightfully laugh in my face and we’ll just use a non ATproto app instead.

This is where some sort of invite/inbox space (either generic, or specific to this shared chess lexicon) makes a lot of sense to me. I can create the new space for a game (either on my PDS or hosted by my app), and then just write the invite.chess.genericlexicon to this invite space that I know their app will be watching. I think this also applies to our shared community lexicons too, but the focused example helps for discussion. Does that example still make this publicly writable invite/inbox space seem like a bad idea?

The spammer does not need to store anything, only present it on demand.

A DID document can either through ephemeral means be spammed into the registry or presented on demand when fetched for a did:web, which only requires the signing key to be derivable from the DID. This association can trivially be opaque if a salted trapdoor function is used.

Similarly, since any calls to getRecord and listRecords (to read atproto space records) clearly identify (through the space credential) the receiver’s space and with that their identity, so an appropriate invite record can be synthesised on the fly without per-invitee storage cost.

That’s true, and I don’t know what the solution there is. Some smaller Telegram groups with public invite link that I’m in get about one spammer a day sometimes, which channel owners mostly address with individual challenge bot setups.

I think for that it’s potentially fine, as long as the chess platform doesn’t allow tappable links or calls to action to be pushed proactively! You could for example say that anyone can play against anyone, but chat has to be initiated by the invitee. (You can shortcut past that if you detect prior association, like the invitee following the inviter on another atproto app.)

I think players will accept that limitation if you clarify what it’s designed to prevent right there with a small ? or such that leads to a tooltip, and you can experiment with the criteria to find the right balance.

Yes, it’s a fair point. I think a potential compromise is to open the inbox up to follows of follows in-app (similarly to Discord’s settings regarding who can send friend requests) and require a (cross-app-compatible) invite code or link (containing that code) to be used only where no prior acceptance or interaction can be established at all.

Fortunately, with the pre-seeded social graph on atproto, I think “(up to) follows-of-follows” (across apps) is going to tend to be a large set and will be biased to include people the invitee is interested in hearing from.

For something like chat groups yes, it’s a pretty terrible feature. I had my group invites set to open for a while on WhatsApp and the only thing I got was financial crime attempts. And atproto accounts are much easier to enumerate and check for recent activity than WhatsApp ones, so I’m pretty certain it would be much worse for any app that becomes somewhat popular, similarly to the commission scam wave, the Discord phishing wave and the charity fraud waves that Bluesky had to deal with.

I think for apps that do not allow arbitrary communication through alongside just the push message, it can be okay, but I think that category is pretty limited. Game invites, as you said, come to mind (at least with the chat limitation I mentioned) and also something like highscore challenges, as long as you give players at least the option to limit them.