Rellm is a social media protocol with support for Users (and Follows), Media, Posts, Events, Groups, and Messages. It is designed to be federated, but does not require federation to be a useful next-gen forum type solution. It is designed to be used with a variety of frontends, including web, mobile, and desktop applications. It interoperates across numerous ports, protocols, and formats, including gRPC, HTTP, HTTPS, and ICS/iCal, and is designed to link with SMTP via Stalwart (and other SMTP servers/providers), Facebook Page APIs for post ing Events, and more. Essentially, your server is your own customizable, self-contained social network.
Rellm is designed to be easy to run and deploy yourself with a 2 minute setup with Homebrew and 3 minute setup on Linux,
images on DockerHub and deployment to your K8s clusters available via
a simple but powerful Makefile-based design language.
Rellm servers interact across several ports:
backend_host HTTP endpoint (see below) on port 80/443./email endpoint. It is designed for email/SMTP support via an integration with Stalwart.
Rellm clients can translate content from other federated protocols into the same
Post/Author shapes used everywhere else in the app --
entirely client-side, with no RPCs of their own. The server's only role is admin configuration:
FederationInfo tells clients which instances/apps are safe or
expected to pull from. There is no server-to-server proxying or bridging involved - this follows
the same "the client does the merging" pattern as FederatedServer,
just reaching across a protocol boundary instead of a Rellm-to-Rellm one. It's also
one-directional (reading in, not posting out) - publishing a Rellm Post to
Mastodon or Bluesky is a separate feature, SyncDestination.
A client can browse any Mastodon instance's local public timeline
(GET /api/v1/timelines/public?local=true) with zero configuration, since it's already a public,
unauthenticated REST endpoint - no MastodonServer entry is needed
just to read public posts.
Connecting an actual Mastodon account is a heavier flow, since Mastodon has no single central
OAuth authority the way Facebook/X do - every instance is its own separate OAuth provider. A
server admin registers an app on a given instance ahead of time
(FederationInfo.mastodon_servers, a MastodonServer carrying that
instance's app_id/app_secret), and only then can a user on that instance complete the OAuth
popup + PKCE flow to connect their own account. MastodonServer.configured_by_default/
pinned_by_default let an admin recommend a given instance be auto-browsed the first time a
client visits this server - see those fields' own docs for the current relationship between the
two.
Unlike Mastodon, AT Protocol has no "local instance timeline" concept at all - every Personal
Data Server (PDS) only ever serves its own users' own repos, so there is nothing equivalent to
browse anonymously. Cross-protocol federation with Bluesky therefore always requires a connected
account: a handle and an App Password (not OAuth --
AT Protocol has no per-client app-registration step the way Mastodon/Facebook/X require), used to
call com.atproto.server.createSession and then the account's own
app.bsky.feed.getTimeline. Because there's no server-side app to register, there is no
BlueskyServer config type mirroring MastodonServer - nothing about
connecting a Bluesky account is admin-configurable the way a Mastodon OAuth app is.
Rellm APIs are designed to support Moderation and Visibility controls at the level of individual entities. However, to keep things
DRY, moderation and visibility controls are only implemented for Users, Media, Groups, and Posts.
Events and future Post-like types simply use the same implementation as their contained Posts. The intent here is to maximize
both shared code and implementation robustness.
Rellm's APIs are designed using composition over inheritance. For instance, an Event contains
a Post rather than extending it. This pattern fits well all the way from the data model (very boring, safe, and normalized),
through Rust code implementing APIs, to both functional React code and more-OOP Flutter code equally well.
The use of composition over inheritance also means that Rellm APIs can be predictably non-atomic based on their compositional structure.
For instance, UpdatePost is fully atomic.
UpdateEvent, however, is non-atomic. Given that an Event has a Post and many Occasions,
UpdateEvent is implemented as a composition of four other RPCs - each independently callable and individually atomic --
run in a fixed order: UpdateEventDetails (which itself first updates the Event's own Post
atomically, literally calling the UpdatePost RPC), then CreateNewOccasions,
UpdateOccasions, and finally DeleteRemovedOccasions.
Create must run before Delete so that a request which both drops an old Occasion and adds a new one never transiently
leaves the Event with zero Occasions.
Because moderation/visibility lives at the Post level, and UpdateEventDetails runs first, this means that a developer error in the
later Occasion-processing steps cannot prevent visibility and moderation changes from being made in Events, even if there are errors elsewhere.
This should prove a robust pattern for any future entities intended to be shareable at a Group level with visibility and
moderation controls (for instance, Sheet, SharedExpenseReport, SharedCalendar, etc.). The entire architecture should promote this
approach to predictable atomicity.
Rellm's data model centers around a handful of top-level types, most of which carry their own
Visibility and Moderation state and can be organized into
Groups.
Rellm incorporates server configuration, including fairly deep customization of the end-user UI/UX, as perhaps its most primitive type.
ServerConfiguration is unlike most of the highly-normalized, minimalist types in the Rellm protocol,
and is more like a document than a row in a database. (That said, every ServerConfiguration change is a row in a database, meaning
reverting broken configurations is easy.)
Any client using the Rellm protocol is basically expected to follow a flow of "get service version, then ServerConfiguration,
then worry about auth, then finally about retrieving anything else."
ServerInfo (server_info) carries the server's public-facing identity: name,
short_name, description, privacy_policy and media_policy text shown during account creation and on the
/about page, a multi-size ServerLogo (separate light/dark, square/wide media IDs), a
ServerColors scheme (primary/navigation accents plus author/admin/moderator name
colors), and web_user_interface choosing which UI a browser is served (React/Tamagui by default, or the Elm
SPA/Flutter Web alternatives).
CustomNavigationTabSet (custom_tabs) lets a server admin override the Elm
UI's default navigation. home (a CustomHomePage) replaces / itself - a
predefined tab or a specific Post, optionally with Posts pinned above its content and/or an Events strip shown
above it; tabs (repeated CustomNavigationTab) replaces the
EVENTS_TAB/POSTS_TAB/PEOPLE_TAB/ABOUT_TAB set entirely, each pinned to its own custom URL (path). Each
CustomNavigationTab targets either a predefined NavigationTab, a Post ID, or
(path-only) a user profile, with its own emoji- or Media-backed icon and optional title override. path is
fully live - the Elm SPA actually routes it (Pages.UsernameOrCustomTab_), not just previews it - except for
the built-in /events, /posts, /people, and /about paths themselves, which stay reserved for their own
matching predefined tab and can't be remapped elsewhere.
Three Permission lists set the server's baseline access, each enforced independently of
any per-User/per-Group grants: anonymous_user_permissions (what a logged-out visitor may do - only the
VIEW_* permissions are valid here), default_user_permissions (what every new account starts with), and
basic_user_permissions (the superset a user holding GRANT_BASIC_PERMISSIONS may hand out to others). Granting
GLOBAL_PUBLIC as a feature's default_visibility (see people_settings/group_settings/post_settings/
event_settings below) requires the matching PUBLISH_*_GLOBALLY permission to actually be present in
default_user_permissions.
FederationInfo (federation_info) is where all federation and social-sync
credentials live.
servers (repeated FederatedServer) recommends other Rellm hosts to clients,
each optionally configured_by_default (client should enable/configure it automatically) and/or
pinned_by_default (client should pin its Events/Posts alongside the "main" server's).
mastodon_servers (repeated MastodonServer) plays a similar role to
servers above, but for Mastodon instances instead of other Rellm servers - see
Cross-Protocol Federation for the client-side feature this backs.
Unlike a real FederatedServer, though, an entry here is not required just to browse an
instance's public timeline read-only - that's already a public, unauthenticated Mastodon REST
endpoint any client can call directly. It's only needed to let a user connect their own
Mastodon account (OAuth + PKCE), since Mastodon has no single central OAuth authority the way
Facebook/X do: every instance is its own separate OAuth provider, so an admin has to register an
app (app_id/app_secret, the latter never serialized to the client) on each instance
individually before its users can connect. configured_by_default/pinned_by_default mirror
FederatedServer's own fields, but govern that anonymous browsing instead: whether clients should
auto-add the instance to their browsed list the first time they visit this server, not whether an
account gets auto-connected (that always requires the user's own explicit OAuth consent).
A Mastodon instance functions like a much thinner version of a federated Rellm server in the
UI: its public posts appear in the same multi-server feed, translated into Rellm's own
Post shape, but it has no equivalent of Rellm's Events, Groups, Media
library, or People/Follows - just posts and their authors.
facebook_auth_config (a FacebookAuthConfig, app_id/app_secret) registers
this server's Facebook App, enabling users to connect Facebook Page and Instagram Business
SyncDestinations. app_secret is write-only/never serialized back to clients;
admins set/rotate it via ConfigureServer (i.e. the same admin UI form that
manages the rest of ServerConfiguration) - the secret is simply never echoed
back in subsequent GetServerConfiguration responses.
x_twitter_auth_config (an XTwitterAuthConfig, client_id/client_secret)
registers this server's X Developer App, enabling users to connect X SyncDestinations
x_twitter_app_not_configured. client_secret is write-only/never
serialized back to clients, set/rotated the same way as the Facebook API keys above.
WebPushConfig (web_push_config) holds the server's VAPID keypair for Web Push
notifications: public_vapid_key is served to clients so they can subscribe, while private_vapid_key signs
outgoing pushes and is never serialized to clients - like the federation secrets above, admins set/rotate it
via ConfigureServer, not by editing the database directly.
ExternalCDNConfig (external_cdn_config) enables running Rellm behind a CDN
(e.g. Cloudflare's "CNAME HTTPS Proxy"): when set, the unsecured HTTP server (port 80) stops redirecting to
HTTPS and instead serves the Tamagui Web client directly, with frontend_host/backend_host telling the web
client which domains to use instead of window.location.hostname (Tamagui web only, for now). secure_media
plus its media_ipv4_allowlist/media_ipv6_allowlist are a (TODO, not yet enforced) way to restrict media
downloads on the unsecured server to the CDN's own IP ranges; cdn_grpc is a further (TODO) mode that would move
the gRPC server itself onto port 443 to ride along Cloudflare's gRPC support.
A User is a Rellm account: username, real name, bio, avatar, contact methods, and
Permissions, plus counts (followers, posts, events, etc.) and federation info (see
Federated Profiles above). A lighter-weight Author (just ID, username,
avatar, real name, permissions) is embedded on Posts, Messages, and similar
content types instead of a full User, to keep those payloads small.
A Follow is one User following another, optionally subject to the target's moderation
(i.e. approval). Mutual follows make two users "friends." Follows also drive the FOLLOWING_POSTS/FOLLOWING_EVENTS
listing types and LIMITED-visibility content.
A Membership is a User's membership (or pending join request/invitation)
in a Group, tracking the user's Permissions within the group plus separate group-side and user-side Moderation
(for join-approval flows). Returned as part of User/Group payloads, and via Member when listing a Group's members.
While Federation is a first-class feature of Rellm, a User can also own many
SyncSources - server-owned external origins to sync Posts in from other
fediverse and less-open platforms, via a oneof configuration naming which source type it is: an iCal
subscription URL (configuration.ics_subscription_url, syncing in Events/
Occasions), or an RSS/Atom subscription URL (configuration.rss_subscription_url/
configuration.atom_subscription_url, syncing in plain Posts). Every kind of synced content is
tagged via its own Post.sync_source - an Event's own Post, each of its
Occasions' own Post, or a plain synced Post - since a single source can back many
synced Posts but each Post has at most one source it came from; see the Event and Post sections below for how
these attach. A background job re-pulls each source on its own sync_interval_seconds cadence, recomputing
event_count/occasion_count (iCal) or post_count (RSS/Atom) on every sync.
Sources are managed via GetSyncSources, CreateSyncSource
(requires SYNC_EVENTS_FROM_ICS/SYNC_POSTS_FROM_RSS/SYNC_POSTS_FROM_ATOM - whichever matches
the source's own configuration - or Admin), UpdateSyncSource, and
DeleteSyncSource.
See also: SyncDestination
configuration.ics_subscription_url is a plain iCal (.ics) subscription URL. The background job fetches and
parses it on each sync, creating/updating one Event (and one Occasion
per occurrence) per iCal VEVENT - each occurrence's own Post is keyed by (sync_source_id, sync_source_uid, sync_source_recurrence_anchor), the iCal UID plus that occurrence's stable identity within its series (its own
start time, or its original scheduled time if since rescheduled) - and recomputing event_count/
occasion_count. An Occasion's sync_missing_since is set the first time it stops appearing in
the feed, letting the owner decide whether that means it should be deleted. No auth/credentials are supported
yet - only public iCal URLs.
configuration.rss_subscription_url is a plain RSS 2.0 subscription URL. The background job fetches and parses
it on each sync, creating/updating one plain Post per RSS <item> - each Post is keyed by
(sync_source_id, sync_source_uid), sync_source_uid being the item's own <guid> (or a hash of its <link>
if it has none) - and recomputing post_count. Unlike iCal, a <item> that stops appearing in the feed is left
alone rather than pruned: RSS feeds are commonly truncated to their most recent N items by the publisher, so
"no longer in the feed" doesn't mean "was retracted". No auth/credentials are supported yet - only public RSS
URLs.
configuration.atom_subscription_url is a plain Atom subscription URL, behaving identically to RSS (above) --
one plain Post per <entry>, keyed by (sync_source_id, sync_source_uid) with sync_source_uid
being the entry's own <id>, recomputing post_count, missing entries left alone rather than pruned. RSS and
Atom feeds are parsed via the same underlying library into one unified shape, so both formats share this exact
behavior - pick whichever a given source actually publishes.
A User can also own many SyncDestinations - user-owned external
targets to push Occasions and Posts out to (see the Event and
Post sections below for how these attach), via a oneof configuration naming which platform it is. This is a
many-to-many relationship: it's each Occasion or Post (not,
say, the parent Event) that syncs out, and each may push to several destinations at once,
tracked per-destination via the repeated Occasion.sync_destinations/Post.sync_destinations (each a
SyncDestinationStatus, carrying the destination's resulting post ID/URL and
last-synced time). Destinations are pushed to on demand rather than synced in bulk on an interval, so
synced_occasion_count/synced_post_count are computed with a COUNT at request time instead of being
recomputed-and-stored. All API keys for these external platforms are stored in
ServerConfiguration's federation_info.
Destinations are managed via GetSyncDestinations,
CreateSyncDestination, UpdateSyncDestination,
and DeleteSyncDestination - each gated on the SYNC_EVENTS_TO_*/
SYNC_POSTS_TO_* permission pair matching the destination's own platform (or Admin; see each platform's own
section below). Actually syncing (or un-syncing) a given Occasion or Post to a destination is a separate
step, via SyncOccasion/
DeleteOccasionSyncDestination and
SyncPost/DeletePostSyncDestination, gated the same
way (the _EVENTS_/_POSTS_ half matching which RPC).
See also: SyncSource
configuration.facebook_page (a FacebookPage) is a connected Facebook Page.
Connecting one requires a short-lived user access token from client-side Facebook Login
(FacebookPage.short_lived_user_access_token), which the server exchanges for a long-lived Page access token; the
short-lived token is write-only and never populated back in responses. Gated on SYNC_EVENTS_TO_FACEBOOK/
SYNC_POSTS_TO_FACEBOOK.
configuration.instagram_account (an InstagramAccount) is a connected Instagram
Business/Creator account. Instagram posting is only possible for an account linked to a Facebook Page, so
connecting one reuses the exact same Facebook Login flow/app credentials as Facebook above - the server exchanges
the token for the chosen Page's access token, then looks up that Page's linked Instagram Business account
(instagram_business_account_id). Unlike Facebook, Instagram's Graph API has no text-only post type; syncing a
Post/Occasion with no attached media fails with instagram_requires_media. Gated on
SYNC_EVENTS_TO_INSTAGRAM/SYNC_POSTS_TO_INSTAGRAM.
configuration.mastodon_account (a MastodonAccount) is a connected Mastodon
account, on any instance the user names (instance_host) - there's no single app to register the way
Facebook/Instagram have one, so connecting one is a user-pasted Personal Access Token
(MastodonAccount.access_token, generated on the user's own instance under Preferences > Development) rather than
an OAuth popup. Gated on SYNC_EVENTS_TO_MASTODON/SYNC_POSTS_TO_MASTODON.
configuration.bluesky_account (a BlueskyAccount) is a connected Bluesky (AT
Protocol) account. Connecting one is a user-supplied "App Password" (BlueskyAccount.app_password, generated at
Settings > App Passwords - not the account's main password) rather than an OAuth popup. Gated on
SYNC_EVENTS_TO_BLUESKY/SYNC_POSTS_TO_BLUESKY.
configuration.x_twitter_account (an XTwitterAccount) is a connected X account. Requires this
server to have a registered X Developer App configured (FederationInfo.x_twitter_auth_config) - until an admin
sets one, every RPC touching an XTwitterAccount destination fails with x_twitter_app_not_configured. Once
configured, connecting is an OAuth 2.0 Authorization Code + PKCE flow at x.com (response_type=code, like
Threads, but with a code_challenge/code_verifier pair X requires and Threads doesn't) - the server exchanges
the code for a short-lived access token (2 hour expiry) plus a refresh token, transparently refreshing before
each post. Only image media is uploaded today; video is not yet supported (see XTwitterAccount's own doc).
Gated on SYNC_EVENTS_TO_X_TWITTER/SYNC_POSTS_TO_X_TWITTER.
configuration.threads_account (a ThreadsAccount) is a connected Threads account.
Threads API is a product added to this server's existing Facebook App (see FacebookAuthConfig) rather than a
separately-registered app, but its OAuth flow is otherwise its own: authorization happens at threads.net (not
facebook.com) using response_type=code rather than Facebook's implicit response_type=token, with no "choose a
Page" step - it directly authorizes the user's own Threads account. The server exchanges the code for a
short-lived token, then a long-lived one (~60 day expiry, refreshable via grant_type=th_refresh_token - not yet
implemented, so a connected destination needs reconnecting after ~60 days). Unlike Instagram, Threads supports
text-only posts. Gated on SYNC_EVENTS_TO_THREADS/SYNC_POSTS_TO_THREADS.
A User can also own many AIProviders -
connections to external AI model APIs (e.g. a Gemini or OpenAI API key) - and grant other users metered access to
them via AIProviderGrants. See ai_providers.proto and the
AIProvider section below. Which models are actually available, and what each can do
(AIModelCapability), is a hand-maintained catalog (no provider exposes a stable
"list models" API to build this from at request time) - see
backend/src/logic/ai_model_catalog.rs
on GitHub for the actual source of truth.
An AIProvider is a user-owned connection to an external AI model API (e.g. a
Gemini API key), via a oneof provider naming which service it is - structurally similar to
SyncDestination/SyncSource, but rather than pushing/pulling
content, it's metered access an owner can share out to other users of this server. As with
SyncDestination's platform credentials, the actual API key is write-only - accepted
on CreateAIProvider/UpdateAIProvider
but never populated back in a response.
Providers are managed via GetAIProviders,
CreateAIProvider (requires CREATE_AI_PROVIDERS, or Admin),
UpdateAIProvider, and DeleteAIProvider
SyncDestination's RPCs.
provider.gemini_credentials (a GeminiCredentials) is a Google Gemini API
connection (ai.google.dev/gemini-api), used for image generation/editing (e.g. generating Event posters) via
its Interactions API.
provider.openai_credentials (an OpenAICredentials) is an OpenAI API connection
(platform.openai.com/docs/guides/image-generation), used for image generation/editing via its Images API (GPT
Image models).
provider.anthropic_credentials (an AnthropicCredentials) is reserved for a
connected Anthropic API, but not yet creatable - Anthropic doesn't offer an image generation API, so it's
defined only for forward compatibility.
provider.digitalocean_credentials (a DigitalOceanCredentials) is a
DigitalOcean Gradient AI Platform / Serverless Inference connection (docs.digitalocean.com/products/inference),
used for image generation only (no editing - DigitalOcean's Serverless Inference API has no
/v1/images/edits-equivalent endpoint) via its OpenAI-Images-API-shaped /v1/images/generations endpoint (GPT
Image and Stable Diffusion models, re-hosted under DigitalOcean's own billing).
A provider's owner may share metered access to it with other users via
AIProviderGrants, each carrying a tokens_remaining budget for that grantee.
Granted/reset via GrantAIProvider (upserted on the unique
(ai_provider_id, grantee) pair - granting again resets, rather than adds to, tokens_remaining) and
removed via RevokeAIProvider. Unlike every other RPC pair in this section,
these two are owner-only, with no Admin override - an Admin may manage the provider record itself, but only
its owner may hand out access to it.
Rellm's Market (market.proto) is this server's storefront - a small, Stripe-backed marketplace an admin
stocks with up to twelve MarketProducts (one offering type times one billing period
each) that any user can buy. Four offering types exist today (PurchaseType):
PURCHASE_TYPE_MEDIA_STORAGE (raises the buyer's User.media_storage_limit_bytes),
PURCHASE_TYPE_AI_GRANTS (grants/resets an AIProviderGrant against one of the
server operator's AIProviders), PURCHASE_TYPE_RELLM_HOSTING (bills the buyer for
the server operator to stand up a new Rellm instance on a domain of their choosing - provisioned by hand, not
automated; the admin's own /market/fulfillment page, backed by
GET_MARKET_SUBSCRIPTIONS_REQUEST_FOR_FULFILLMENT_ADMIN and
UpdateMarketSubscription, tracks these orders -
RellmHostingSubscriptionDetails's own fulfillment_status/
fulfillment_notes fields), and PURCHASE_TYPE_PERMISSIONS_ACCESS (grants the buyer a fixed set of Permissions, e.g.
pay-gating SYNC_EVENTS_TO_FACEBOOK). Every purchase gets a MarketSubscription --
even a one-time (PurchasePeriod PURCHASE_PERIOD_INDEFINITE) purchase, so it's still
cancelable and still carries the same per-type fulfillment tracking (e.g. fulfillment_status/fulfillment_notes above)
a recurring one does; it just never gets a renews_at, so it never actually bills again. A recurring
PURCHASE_PERIOD_ANNUAL/PURCHASE_PERIOD_MONTHLY subscription's billing_history accumulates one
MarketPurchase per renewal; an indefinite one's stays at exactly one.
Products are listed via GetMarketProducts (unauthenticated; admins
additionally see delisted ones) and managed via
CreateMarketProduct/UpdateMarketProduct
(both Admin-only; a MarketProduct's type/period are immutable once created).
Buying one is a two-step, webhook-settled flow: MakeMarketPurchase starts a
Stripe Checkout Session and returns its URL to redirect the buyer to - no
MarketPurchase/MarketSubscription is created yet, so
an abandoned checkout leaves nothing behind. Only once Stripe confirms payment (a
checkout.session.completed webhook delivery) does the server create the
MarketPurchase/MarketSubscription and apply its
entitlement. A recurring MarketSubscription renews itself thereafter via
off-session charges against the payment method saved on that first checkout - no further action from the
buyer - until either a renewal charge fails or the buyer/admin calls
CancelMarketSubscription, either of which sets canceled_at. The
entitlement itself stays in effect until whichever is later of renews_at/canceled_at - once both have
passed, the renew_market_subscriptions background job revokes it (reverts media_storage_limit_bytes to
ServerConfiguration.media_settings.default_media_allocation_bytes, or removes the granted Permissions,
depending on type) and sets service_terminated_at. A user's own MarketSubscriptions (never anyone
else's) are listed via GetMarketSubscriptions, and also travel along
on User itself (User.market_subscriptions) the same way ai_models/sync_sources do.
Stripe credentials for all of this live in ServerConfiguration.stripe_config (a
StripeConfig), Admin-only like twilio_config/bird_config.
Media represents an uploaded (or server-generated) photo or video. Unlike other types, Media
content itself is not served over gRPC - it's uploaded/downloaded via plain HTTP (POST/GET /media) - while
its metadata (content type, name, visibility, moderation) is managed like any other Rellm type. Other messages
(like User.avatar, Group.avatar, and Post.media) reference Media via the lightweight MediaReference type.
Post is Rellm's fundamental content/building-block type: it's what actually carries a
title/link/content body, visibility, and moderation, and is reused (via PostContext) as the backing data for
replies, Events, and Occasions alike. Posts can be replied to (threaded via
reply_to_post_id), cross-posted to Groups (GroupPost), and shared directly with users (UserPost).
A GroupPost is the cross-posting of a Post into a Group, carrying the group-specific
moderation status and who shared it, separately from the Post's own (author-set) visibility/moderation.
A UserPost is a "direct share" of a Post to a User (see also DIRECT
Visibility). Currently unused/unimplemented.
A Post may also be synced (cross-posted) out to a user-owned
SyncDestination (e.g. a connected Facebook Page), the same mechanism
Occasions use (see below) - each Post may push to several destinations at once, tracked via the
repeated Post.sync_destinations (each a SyncDestinationStatus).
An Event is a wrapper for at least two Posts. It always has its own top-level Post
(PostContext.EVENT, holding the event's overall title/description) and it must have at least one
Occasion (see below), each of which in turn must have its own Post
(PostContext.OCCASION, carrying that Occasion's start/end time, Location, and optional per-Occasion
title/link/content override). So the smallest possible Event already backs 2 Posts, and events with recurring/multiple
Occasions back one Post per Occasion beyond that.
An Occasion is the actual time-boxed occurrence of an Event -
it carries the starts_at/ends_at timestamps and optional Location that the parent Event itself does not have.
An Event with zero Occasions is meaningless (no time or place to attach to), so every Event must have at least one.
- **EventAttendances**: An [`EventAttendance`](#rellm-EventAttendance) (an "RSVP") tracks one attendee's status
(`INTERESTED`, `REQUESTED`, `GOING`, `NOT_GOING`) for a specific [`Occasion`](#rellm-Occasion). Attendees may be logged-in [`User`](#rellm-User)s
or anonymous (tracked via [`AnonymousAttendee`](#rellm-AnonymousAttendee) plus an `auth_token`), and are subject to their own [`Moderation`](#rellm-Moderation),
independent of the Event's/Occasion's own Post moderation.
- **SyncSource**: It's actually the parent [`Event`](#rellm-Event) (not the [`Occasion`](#rellm-Occasion)) that can be synced *in* from a
user-owned [`SyncSource`](#rellm-SyncSource) (e.g. an iCal subscription). The relationship is
1:(0 or 1): a single source can back many synced [`Event`](#rellm-Event)s, but each [`Event`](#rellm-Event) has *at most one* source it came from
(`Event.sync_source` is a single optional field, not repeated).
- **SyncDestinations**: Conversely, it's each [`Occasion`](#rellm-Occasion) (not the parent [`Event`](#rellm-Event)) that syncs *out* to
[`SyncDestination`](#rellm-SyncDestination)s (e.g. connected Facebook Pages) - the same mechanism [`Post`](#rellm-Post)s use
(see above). Unlike [`SyncSource`](#rellm-SyncSource), this is the outlier's counterpart - a many-to-many relationship: each
Occasion may push to several destinations at once, tracked per-destination via the repeated
`Occasion.sync_destinations` (each a [`SyncDestinationStatus`](#rellm-SyncDestinationStatus)), carrying
the destination's resulting post ID/URL and last-synced time.
A Group organizes Users, Posts, and Events together under shared visibility, moderation,
and permission defaults.
A Membership is a User's membership (or pending join request/invitation)
in a Group, tracking the user's Permissions within the group plus separate group-side and user-side Moderation
(for join-approval flows). Returned as part of User/Group payloads, and via Member when listing a Group's members.
A GroupPost is the cross-posting of a Post into a Group, carrying the group-specific
moderation status and who shared it, separately from the Post's own (author-set) visibility/moderation.
Message is Rellm's "low trust" messaging/email system, meant to let strangers on a server
make first contact (e.g. via email, with no account required) before moving to a more trusted channel. Admins have
open access to all Messages on a server.
Email support in Messages comes from the Stalwart integration and requires a Stalwart server to be running and configured to forward emails to the Rellm server. Rellm provides tooling to do this automatically, but it is completely optional.
A MessagingGroup is the set of participants in a Message conversation. Every Message
belongs to one; if a client wasn't a visible recipient (e.g. they were BCC'ed), the Message they receive omits it.
Rellm uses a standard OAuth2 flow (over gRPC) for authentication, with rotating access_tokens and refresh_tokens (both ExpirableTokens).
Authenticated calls require an access_token in request metadata to be included / directly as the value of the
authorization header (no Bearer prefix).
The ExpirableToken type allows clients to know ahead of time when their access_token and refresh_token are about to expire.
First, before any authentication is done, you should resolve your backend host,
and check its GetServiceVersion and GetServerConfiguration RPCs.
Check whether you have the CREATE_ACCOUNT and/or LOGIN AuthenticationFeatures in your ServerConfiguration.
Next, use the CreateAccount or Login RPCs to fetch (and store) an initial
refresh_token and access_token. Clients should use the access_token until it expires,
then use the refresh_token to call the AccessToken RPC for a new one. (The AccessToken RPC
may, at random, also return a new refresh_token. If so, it should immediately replace the old
one in client storage.)
tl;dr: Lets you sign in to the [email protected] user on jonline.io, without ever entering your bullcity.social
credentials on jonline.io.
Elm-only feature (frontends/elm-spa) letting a user sign in to one Rellm server using an account they already
have (or are willing to create) on a different Rellm server, without either backend ever seeing a plaintext
token that isn't its own. It's pure browser-to-browser: two Elm SPA page routes
(/auth/to/... and
/auth/from/...) exchange
an encrypted pair of tokens via a full-page redirect; no gRPC/HTTP endpoint on either backend is involved beyond
the Login RPC itself (plus GetCurrentUser on the receiving side,
to hydrate everything else - see step 6).
jonline.io, adding a new account, and enters bullcity.social as the server. Since that
isn't the current host, the Accounts panel offers a "Sign in via bullcity.social" button instead of (or alongside)
a normal username/password form.
bullcity.social, carrying jonline.io's ECDH public key (freshly
generated in-browser and persisted for this purpose) and its own hostname in the URL: /auth/to/{public_key}@jonline.io.
bullcity.social shows its own sign-in form (or, if already signed in there, a badge to reuse that session),
plus a "Sign back in here" checkbox, checked by default.
Login RPC. This always issues a fresh refresh_token/
access_token pair, reserved purely for transfer back to jonline.io - it's never used to sign the browser into
bullcity.social itself. If "Sign back in here" is checked, a second, independent Login
call also runs, so bullcity.social gets its own local session too, and the two servers never end up sharing a
token pair. (Hence "1-2 refresh tokens.")
bullcity.social's hostname and that fresh refresh_token/access_token pair are JSON-encoded and
encrypted to jonline.io's public key from step 2 (ephemeral ECDH + HKDF + AES-GCM - see below) - nothing else
about the account travels in the payload. The browser is then redirected back to jonline.io at
/auth/from/{ciphertext}.
jonline.io decrypts the payload with the private key it generated in step 2, calls
GetCurrentUser against bullcity.social with the decrypted access_token to hydrate
the rest of the account (user ID, username, avatar, permissions, etc. straight from bullcity.social itself
rather than trusting a client-supplied copy of them), then adds it to its Accounts panel and navigates the user
onward - no confirmation step. Either way, the one-time keypair generated in step 2 is discarded and a fresh one
generated in its place, so it can't be reused for a second transfer.
See the two Web UI page routes below for the exact URL/crypto shape.
Whereas other federated social networks (e.g. ActivityPub) have both client-server and server-server APIs, Rellm only has client-server APIs. While server-to-server communication is possible, nothing but some "nice to have" features require it, so it is not used.
Rellm servers can recommend other servers to clients with the federation_info field (a FederationInfo message) in ServerConfiguration.
Clients can use this information to discover other servers, or users can add new servers manually.
Note that, at least for web clients, this means everything is subject to CORS. In the future, Rellm will
allow CORS to be configured in a "strict" mode, so someone else's Rellm server cannot be used to access your server's data
unless you explicitly allow it.
Rellm users can federate with users on any other Rellm server. This works by two-way verification:
For example, Jon has the user jonline.io/jon, oakcity.social/jon,
and bullcity.social/jon associated with one another.
The UI will only show federated profiles if both use profiles have federated with one another.
This mechanism also allows users to link multiple profiles on the same server together. For instance, bullcity.social/jon
and bullcity.social/openmic are linked together, but bullcity.social/openmic
isn't linked to jonline.io/jon or oakcity.social/jon.
Federated profiles are managed via the federated_profiles field (a repeated FederatedAccount) in the User message.
Rellm's protocols and UI are designed to work together to present a seamless UX for content from many types of communities. Users can add/remove servers in a way that gives them control, transparency and trust. Meanwhile, server owners get extreme customization and useful integrations with social media platforms.
Rellm's Elm Messaging UI is generally a multi-server federated messenger. The main limitation is that it can only receive push notifications from one server. (This could be changed with VAPID key sharing, but is part of the VAPID protocol.)
The Elm /market page can show more than one server's Market side by side: the browsed server's own
(always first) plus any other connected, enabled server whose ServerConfiguration.market_settings.enabled
is set (in Accounts panel order after that) - the public, non-secret "is this server's Market open" signal,
unlike stripe_config itself, which stays Admin-only. Each section is fully independent: it lists that
server's own MarketProducts and shows product-management controls only if the
current user is actually an Admin on that server.
Unlike Federated Browsing/Profiles/Messaging above, buying is never seamless across servers - Market is the
one place a user must always transact with the target site directly. A product tile for the browsed server
links to its own in-app product page as usual, but a tile for any other server links straight to that
server's own https://{host}/market/product/{id} (a real page navigation, not client-side routing), since
MakeMarketPurchase starts a Stripe Checkout Session scoped to whichever
server the buyer is actually authenticated against, and Stripe's own success_url/cancel_url redirect
back to that same server when payment completes.
POST /email: Stalwart Email Integration
Delivery endpoint called by the Stalwart mail server (see
deploys/email's
README for setup/architecture) once it
accepts an inbound message addressed to one of this Rellm instance's onboarded domains, turning it into a
Message. It is internal-only: mounted solely on the unsecured 27705 server (never on
80/8000/443), has no authentication of its own, and trusts its caller completely - that trust boundary is
expected to be enforced at the network layer (e.g. a NetworkPolicy restricting port 27705 to Stalwart's pod).
data-stage MTA Hook JSON
payload (up to 50 MiB), not a raw MIME stream - only the fields below are read, the rest of Stalwart's payload
(context, envelope.from, message.serverHeaders, message.size, ...) is ignored:
{
"envelope": { "to": [{ "address": "[email protected]" }] },
"message": {
"headers": [["Subject", "Hello"], ["From", "[email protected]"], ["To", "[email protected]"]],
"contents": "Hello, World!\r\n"
}
}
Recipients come from envelope.to[].address - deliberately the SMTP envelope, not the message's To/Cc
headers, since that's the only place Bcc'd recipients show up at all. The message itself is reconstructed by
concatenating message.headers (each an unfolded [name, value] pair) with message.contents across a blank
line, which mail_parser then parses as the RFC822 message - Stalwart only splits at the top-level header/body
boundary, so this still captures multipart bodies and attachments intact within contents. A body that isn't
valid JSON in this shape, or that doesn't reconstruct into a parseable MIME message, returns 400 Bad Request;
an oversized body returns 413 Payload Too Large.
@) is looked up as a username on this
server; addresses that don't match any user are silently skipped, since Stalwart is expected to have already
confirmed deliverability before calling this endpoint. If none match, the whole message is dropped and the
endpoint returns 404 Not Found.
Message addressed to a
MessagingGroup keyed on the To/Cc recipients only - Bcc'd recipients are excluded
from the group (so they stay invisible to everyone else on the thread) and instead recorded individually as Bcc
rows on the Message. The Message has no from_user_id, since inbound email never has a local sender; its
parsed from/to/cc headers are stored alongside it, and the raw .eml is uploaded to the same MinIO store
used for Media. Duplicate deliveries of the same Message-ID (Stalwart retries on transient failure) reuse the
existing Message row rather than storing/uploading a duplicate.
200 OK with a body of {"action": "accept"} on success - Stalwart's MTA Hook protocol parses
the response body, not just the status code, so this has to be the exact shape it expects
(see <https://stalw.art/docs/mta/filter/mtahooks/>) or Stalwart treats the call as a hook failure regardless of
status; combined with the MtaHook's tempFailOnError: true, that surfaces to the sending client as a
451 temp-fail rather than anything indicating the real cause.
Note that, if the TLS server on port 443 starts up successfully, the server on port 80 will simply redirect to HTTPS.
The server on port 8000 will always serve up unsecured HTTP. It is up to server admins to block this port if they find that necessary.
GET /backend_host: HTTP-based client host negotiation (for external CDNs)
When first negotiating the gRPC connection to a host, say, jonline.io, before attempting
to connect to jonline.io via gRPC on 27707/443, the client
is expected to first attempt to GET jonline.io/backend_host over HTTP (port 80) or HTTPS (port 443)
(depending upon whether the gRPC server is expected to have TLS). If the backend_host string resource
is a valid domain, say, jonline.io.itsj.online, the client is expected to connect
to jonline.io.itsj.online on port 27707/443 instead. To users, the server should still generally appear to
be jonline.io. The client can trust jonline.io/backend_host to always point to the correct backend host for
jonline.io.
This negotiation enables support for external CDNs as frontends. See https://jonline.io/about?section=cdn for more information about external CDN setup. Developers may wish to review the React/Tamagui and Flutter client implementations of this negotiation.
GET /robots.txt: Robots
Generated on the fly (not a static file) from the request's Host header, publicly cacheable for 1 hour. Always
allows all crawling (User-agent: * / Allow: /) and points crawlers at https://{host}/sitemap.xml.
GET /sitemap.xml: Sitemap
Generated on the fly (not a static file) from the request's Host header, publicly cacheable for 1 hour. Lists a
fixed set of top-level, server-wide pages - /, /posts, /events, /people, /about, /about_rellm,
/flutter, /tamagui, /elm - plus any CustomNavigationTabSet.tabs paths configured on the server (excluding
the reserved posts/events/people/about paths, which are always included above), each qualified with the
request's Host. It also enumerates individual pages: every Post from an unauthenticated GetPosts (the same
"first page" an anonymous visitor sees) as /post/{id}, and every Occasion from an unauthenticated
GetEvents starting EventSettings.calendar_lookback_days (or 14, if unset) ago as /event/{occasion_id}.
It does not (yet) enumerate individual User pages.
GET /favicon.ico: ICO Favicon
Serves the server's configured logo (ServerConfiguration.server_info.logo.square_media_id, a Media
reference) as an .ico, publicly cacheable for 12 hours (must-revalidate), converting on the fly if the
stored rendition is a .png. If no logo is configured, falls back to the bundled Tamagui frontend's default
favicon instead. Whichever converted rendition of the logo is served, it's picked in size preference order
Medium, then Small, then Large, then the original upload if none of those conversions exist (favicons are small,
so there's no reason to prefer a bigger one).
GET /favicon.png: PNG FaviconAs GET /favicon.ico above, but serves (and if necessary converts to) .png instead.
POST /media: Upload Media
See the Media section for the Media type itself; this is how its bytes actually get in
(an OPTIONS /media variant also exists, solely to satisfy CORS preflight requests). Authenticated (via
Authorization header or a rellm_access_token cookie). Requires Content-Type and Filename headers; the
body is streamed directly to the object store, capped at 250 MiB - note that a larger upload is silently
truncated to that cap rather than rejected, since nothing checks for completeness the way POST /email does --
at a path namespaced by uploader and request host (user/{user_id}@{host}-{username}/{uuid}-{filename}). A
Media row is created immediately at GLOBAL_PUBLIC visibility (video content types also get
a default video_preview_time_ms) and its ID returned as plain text - there's no separate "confirm" step, and
no image/video conversion happens synchronously on this request (see the background media-conversion job).
GET /media/{id}?size={original|small|medium|large}: Download Media(An OPTIONS /media/{id} variant also exists, solely to satisfy CORS preflight requests.) Publicly downloadable
TODO in media_file's
implementation), so a Media ID is currently a bearer capability. size (default medium) selects a converted
rendition, falling back to the original upload if that conversion doesn't exist. The first request for a given
rendition lazily downloads it from the object store into a local on-disk cache; subsequent requests are served
from that cache. Cacheable for 12 hours (must-revalidate).
GET /calendar.ics: Server Calendar
Rellm events support iCalendar/RFC5545; only public events are included. "Subscribe" to a Rellm server at,
for instance, https://jonline.io/calendar.ics to get a calendar of all public events on the server. In the
Tamagui/React frontend, links to these endpoints are provided in the Upcoming Events section of the home page,
the Events page, and the user profile pages for all users with events in the last 3 months (or in the future).
GET /calendar.ics?user_id={id}: User Calendar
"Subscribe" to a user's calendar at, for instance, https://jonline.io/calendar.ics?user_id=CruFm to get a
calendar of all public events for that user.
GET /rss.xml / GET /atom.xml: Server Posts Feed
The reverse direction of a SyncSource's own RSS/Atom subscription (see the SyncSources section above): serves
Rellm's own Posts back out as a feed, only public Posts included. "Subscribe" to a Rellm server
at, for instance, https://jonline.io/rss.xml (or /atom.xml) to get a feed of all public posts on the server,
in whichever of the two formats a given feed reader prefers - both endpoints serve the same underlying Posts.
In the Elm frontend, links to these endpoints are provided next to the Posts page's own search controls, and on
the home page and user profile pages' embedded posts lists.
GET /rss.xml?user_id={id} / GET /atom.xml?user_id={id}: User Posts Feed
"Subscribe" to a user's posts at, for instance, https://jonline.io/rss.xml?user_id=CruFm (or /atom.xml?user_id=CruFm)
to get a feed of all public posts for that user.
Rellm serves three web frontends from the same backend: Tamagui (React/Next.js), Elm, and Flutter.
Tamagui and Elm share one page structure (below) and are always both reachable, explicitly, at /tamagui/*
and /elm/* respectively; unprefixed requests (/, /posts, /post/{postId}, etc.) render whichever of the
two the server's ServerConfiguration.server_info.web_user_interface selects (ELM_SPA picks Elm; every other
setting, including no preference at all, picks Tamagui). Elm is a genuine single-page app - every Elm-served
path, prefixed or not, resolves to the same index.html, with in-app (client-side) routing taking over from
there - whereas Tamagui's Next.js build is statically exported one HTML file per route, so the server picks
between actual distinct files below, each enriched with server-rendered, per-route social-preview
(<title>/og:*) tags before being served.
Flutter does not participate in any of this. It has no page structure of its own to speak of: no per-route
pages, no server-rendered social-preview metadata, and no unprefixed presence at all - a server configured to
prefer it doesn't route "/" through the Tamagui/Elm machinery below and then render Flutter, it instead serves
Flutter's own index.html directly, bypassing that machinery entirely. Flutter is otherwise reached only at the
literal /flutter and /flutter/* paths, which serve its compiled static assets; from there, all further
in-app navigation is handled entirely client-side by Flutter's own router and is invisible to the server.
The shared Tamagui/Elm page structure, grouped the way the Elm app's Pages
directory is ({name} denotes a
dynamic path segment; [@{host}] marks where a federated {username}@{host}-style
suffix is accepted for that segment):
/: HomeThe community's latest activity.
/posts: PostsThe Posts listing.
/post/{postId}[@{host}]: Post
An individual Post - including Event/Occasion posts and replies, which are Posts
themselves (see Post above).
/events: EventsThe Events listing.
/[-._~:/?[]@!$&'()*+,;%=]{postId}: Short Post/Event URLs
A Post or Event/Occasion, reached at its own post.id prefixed
with any single character a username/custom tab path could never legally start with (see
validate_username's
own reserved-lead-character check) - e.g. jonline.io/:4rAfoSKAuJo or ato.band/~4rAfoSKAuJo.
This is purely a shorter, friendlier alias for /post/{postId}[@{host}] or
/event/{postId}[@{host}] (whichever the id turns out to belong to) - it renders exactly that
same content in place, without redirecting the address bar away from the short URL. # is
deliberately excluded from the reserved set: URL fragments never reach the server, so they
can't be used for this.
/event/{postId}[@{host}]: EventAn individual Event, looked up by its own post.id or any of its Occasions' post.ids.
/event_ai: AI Event ImporterTamagui-only, for now - an AI-assisted bulk Event importer. Elm doesn't have this page yet.
/people: PeopleThe People listing.
/people/follow_requests: Follow RequestsThe current user's pending Follow requests.
/user/{userId}: ProfileA User profile looked up by (stable) user ID.
/{custom_tab_or_username}: User pages by username, or a custom tab
The same User profile (and its Posts/Friends/Followers/Following sub-pages) as /user/{userId} above, but
looked up by the current username instead - lighter-weight to link to, but less stable than /user/{userId}
since a username can change. This single path segment is also the server's last-resort catch-all, resolved in
order: first any actual matching build asset or other explicit route above (e.g. /posts, /user/{userId})
wins outright; then, if none matched, an admin-configured custom tab path (see
CustomNavigationTab.path) - e.g. a band mounting their Events
listing at /gigs - wins over a same-named user; only then, last, is it looked up as a plain username. A small
set of reserved names can never be reached this way, only via /user/{userId}.
/{username}/posts: Posts/{username}/friends: Friends/{username}/followers: Followers/{username}/following: Following/g/{shortname}: GroupsA Group's pages. Tamagui-only for now - the Elm frontend doesn't have Group pages yet.
/g/{shortname}: Home/g/{shortname}/posts: Posts/g/{shortname}/p/{postId}[@{host}]: PostAn individual Post cross-posted into the group.
/g/{shortname}/events: Events/g/{shortname}/e/{occasionId}[@{host}]: Event/g/{shortname}/members: Members/g/{shortname}/m/{username}: MemberAn individual Member's details.
/server/{serverIdentifier}: ServerInformation about a (possibly federated) Rellm server.
/about, /about_rellm: AboutThis server's own About page, and a general "what is Rellm" page.
/auth/to/{public_key}@{requesting_host} and /auth/from/{encrypted_account_auth_tokens}: Federated Sign-In
Elm-only - unlike everything else in this section, these two paths have no Tamagui equivalent. They're Elm
SPA pages (served like any other SPA route - under the /elm base path when the Elm frontend isn't the one
mounted at /) rather than backend/gRPC handlers, driving the
Federated Authentication flow entirely in-browser via a pair of full-page redirects
carrying an encrypted payload.
/auth/to/{public_key}@{requesting_host}: sending side
Pages.Auth.To.Key_. Reached only via the cross-origin redirect from step 2 above (built by the requesting
origin's Accounts panel), never linked to directly.
{public_key} is the requesting origin's ECDH (P-256) public key, raw-exported and
base64url-encoded; {requesting_host} is that origin's own hostname. The two are joined with a literal @
(chosen because @ never appears in the base64url/dot-joined ciphertext the
/auth/from page below expects, so the split is
unambiguous).
start_path - the app-relative path the user was on when they clicked "Sign in via ...", so
they can be dropped back there after the round trip. Percent-encoded; passed through unchanged to the eventual
/auth/from redirect.
Login RPC (always a fresh login, never reusing stored tokens) to mint the transfer
tokens; if the checkbox is checked, a second independent Login call also signs the browser
into this server locally. {requesting_host}'s hostname plus that fresh refresh_token/access_token pair are
then AES-GCM-encrypted to {public_key} (fresh ephemeral ECDH keypair per encryption, shared secret via
ECDH + HKDF-SHA256, output ephemeral_public_key.iv.ciphertext, each part base64url) and the browser is
redirected to https://{requesting_host}/auth/from/{ciphertext}?start_path={start_path}.
/auth/from/{encrypted_account_auth_tokens}: receiving side
Pages.Auth.From.EncryptedAccountAuthTokens_, closing the loop from
/auth/to above. Reached only via that redirect.
{encrypted_account_auth_tokens} is the ephemeral_public_key.iv.ciphertext blob produced by
/auth/to.
start_path, passed through unchanged from
/auth/to; defaults to / if missing.
{encrypted_account_auth_tokens} using the private key this origin generated when it
built the /auth/to link (same ECDH + HKDF-SHA256 + AES-GCM
derivation, in reverse), yielding bullcity.social's hostname and its refresh_token/access_token. Calls
GetCurrentUser against that server with the decrypted access_token to hydrate the
rest of the account, then adds it straight to the local Accounts panel and navigates to start_path - no
confirmation step (decryption succeeding is itself the authenticity check: the ciphertext is AEAD-encrypted to
this origin's own one-time private key, so a forged or replayed payload just fails to decrypt rather than
producing a wrong-but-valid account). If either step fails (bad decrypt, or the GetCurrentUser call itself),
an error is shown instead. Either way, once the flow completes (added, or failed), the one-time private key is
discarded and a fresh keypair generated, so it's single-use per completed/failed transfer.
| Method Name | Request Type | Response Type | Description |
|---|---|---|---|
| GetServiceVersion | .google.protobuf.Empty | GetServiceVersionResponse | Get the version (from Cargo) of the Rellm service. Publicly accessible. |
| GetServerConfiguration | .google.protobuf.Empty | ServerConfiguration | Gets the Rellm server's ServerConfiguration. Publicly accessible -- some fields (e.g. twilio_config, preferred_verification_apis) are stripped for a non-admin caller, see that message's own field docs. |
| CreateAccount | CreateAccountRequest | RefreshTokenResponse | Creates a user account and provides a refresh_token (along with an access_token). Publicly accessible. |
| Login | LoginRequest | RefreshTokenResponse | Logs in a user and provides a refresh_token (along with an access_token). Publicly accessible. |
| AccessToken | AccessTokenRequest | AccessTokenResponse | Gets a new access_token (and possibly a new refresh_token, which should replace the old one in client storage), given a refresh_token. Publicly accessible. |
| GetCurrentUser | .google.protobuf.Empty | User | Gets the current user. Authenticated. |
| ResetPassword | ResetPasswordRequest | .google.protobuf.Empty | Resets the current user's - or, for admins, a given user's - password. Authenticated. |
| GetMedia | GetMediaRequest | GetMediaResponse | Gets Media (Images, Videos, etc) uploaded/owned by the current user. Authenticated. To upload/download actual Media blob/binary data, use the HTTP Media APIs. |
| DeleteMedia | Media | .google.protobuf.Empty | Deletes a media item by ID. Authenticated. Note that media may still be accessible for 12 hours after deletes are requested, as separate jobs clean it up from S3/MinIO. Deleting other users' media requires ADMIN permissions. |
| UpdateMedia | Media | Media | Updates a Media item's name/description/metadata.video_preview_time_ms by ID. Authenticated. Every other field (visibility, moderation, sizes, etc.) is ignored -- use other RPCs (or, for sizes, DeleteMediaSizes) to change them. If metadata is set and its video_preview_time_ms differs from the item's current value, any existing VIDEO_PREVIEW_THUMBNAIL_* sizes are deleted (both from sizes and their backing MinIO objects) so convert_media_sizes regenerates them at the new time -- see MediaMetadata and MediaConversion's own docs. Updating other users' media requires ADMIN permissions. |
| DeleteMediaSizes | Media | Media | Deletes only the given sizes (matched by conversion) of a Media item by ID, e.g. to reclaim space by dropping MEDIA_CONVERSION_ORIGINAL once converted copies exist to serve in its place. Authenticated. Deleting other users' media requires ADMIN permissions. Errors if this would leave the Media item with no sizes at all -- use DeleteMedia to remove the whole item instead. |
| GetUsers | GetUsersRequest | GetUsersResponse | Gets Users. Publicly accessible or Authenticated. Unauthenticated calls only return Users of GLOBAL_PUBLIC visibility. |
| UpdateUser | User | User | Update a user by ID. Authenticated. Updating other users requires ADMIN permissions. |
| StartContactMethodVerification | ContactMethod | ContactMethod | Starts SMS verification of the current user's own phone ContactMethod. Authenticated, self-only. Requires the server to have a TwilioConfig/BirdConfig/ TelnyxConfig SMS provider configured and enabled (see ContactProtocol for the corresponding server-wide toggle). Also requires the phone ContactMethod's own consent_state to already be CONTACT_CONSENT_GRANTED -- fails with contact_consent_not_granted otherwise, since the verification code is itself an outbound SMS sent through that same provider (see ContactMethod.consent_state's own doc; this is checked even though the user is the one requesting the send). Generates a 6-digit code, sends it via SMS, and stores it (with a start time and attempt counter) on the phone ContactMethod (verification_in_progress). Only tel: values are supported this iteration -- mailto: returns Unimplemented. Rate-limited to one send per 60 seconds per user. |
| VerifyContactMethod | VerifyContactMethodRequest | ContactMethod | Verifies a code sent by StartContactMethodVerification. Authenticated, self-only. On match, sets ContactMethod.verified_at and clears verification_in_progress. Codes expire after 10 minutes and allow at most 5 attempts before requiring a fresh StartContactMethodVerification call. |
| DeleteUser | User | .google.protobuf.Empty | Deletes a user by ID. Authenticated. Deleting other users requires ADMIN permissions. |
| SendMessage | SendMessageRequest | Message | Sends a Message to one or more recipients (creating/reusing their MessagingGroup). Publicly accessible or Authenticated. Like CreatePost/CreateEvent, authentication (if any) is via a standard access_token; unauthenticated calls are simply sent with no sender. |
| GetMessages | GetMessagesRequest | GetMessagesResponse | Gets Messages. Authenticated. PERSONAL_MESSAGES(_TEXT_SEARCH) (and looking up a single Message/MessagingGroup) requires the READ_PERSONAL_MESSAGES permission and only returns Messages the current user sent or received. ALL_SYSTEM_MESSAGES(_TEXT_SEARCH) requires the READ_ALL_SYSTEM_MESSAGES permission and returns every Message on the server. |
| MarkMessagesRead | MarkMessagesReadRequest | MarkMessagesReadResponse | Marks one or more Messages as read (or unread) by the current user, e.g. every message in a thread once it's been opened. Authenticated. Only needs the recipient/sender access GetMessages already requires for each Message - no separate permission. Atomic: if the caller lacks access to any of message_ids, none of them are marked (matching MarkMessagesReadRequest.message_ids' own doc), so a client never has to reconcile a partially-applied batch. |
| RegisterPushSubscription | RegisterPushSubscriptionRequest | PushSubscription | Registers (or re-registers) a browser's Web Push subscription for the current user, so new Messages sent/delivered to them (in-app or via email) push a notification to it even while the browser tab is closed. Authenticated. Re-registering an already-registered endpoint (e.g. because PushManager.subscribe() refreshed its keys) updates it in place rather than erroring. No-ops (server-side; not surfaced as an error to the caller) if the server has no WebPushConfig configured - there's nothing to push notifications with. |
| UnregisterPushSubscription | UnregisterPushSubscriptionRequest | .google.protobuf.Empty | Unregisters a browser's Web Push subscription, e.g. on logout or when PushManager.subscribe() reports the subscription as no longer valid. Authenticated. Not an error if endpoint isn't currently registered to the calling user. |
| GetPushSubscriptionStatus | GetPushSubscriptionStatusRequest | GetPushSubscriptionStatusResponse | Checks whether the calling user specifically (not just "some account on this browser") has a PushSubscription registered for endpoint. Authenticated. Exists because a browser only ever exposes its own subscription's endpoint/keys, never who on the server side is registered against it - multiple local accounts on the same server can share one browser subscription (see RegisterPushSubscription's own doc comment), so knowing the endpoint alone isn't enough to know which of them are actually notified by it. |
| CreateFollow | Follow | Follow | Follow (or request to follow) a user. Authenticated. |
| UpdateFollow | Follow | Follow | Used to approve follow requests. Authenticated. |
| DeleteFollow | Follow | .google.protobuf.Empty | Unfollow (or unrequest) a user. Authenticated. |
| GetGroups | GetGroupsRequest | GetGroupsResponse | Gets Groups. Publicly accessible or Authenticated. Unauthenticated calls only return Groups of GLOBAL_PUBLIC visibility. |
| CreateGroup | Group | Group | Creates a group with the current user as its admin. Authenticated. Requires the CREATE_GROUPS permission. |
| UpdateGroup | Group | Group | Update a Groups's information, default membership permissions or moderation. Authenticated. Requires ADMIN permissions within the group, or ADMIN permissions for the user. |
| DeleteGroup | Group | .google.protobuf.Empty | Delete a Group. Authenticated. Requires ADMIN permissions within the group, or ADMIN permissions for the user. |
| GetMembers | GetMembersRequest | GetMembersResponse | Get Members (User+Membership) of a Group. Publicly accessible or Authenticated. |
| CreateMembership | Membership | Membership | Requests to join a group (or joins it), or sends an invite to the user. Authenticated. Memberships and moderations are set to their defaults. |
| UpdateMembership | Membership | Membership | Update aspects of a user's membership. Authenticated. Updating permissions requires ADMIN permissions within the group, or ADMIN permissions for the user. Updating moderation (approving/denying/banning) requires the same, or MODERATE_USERS permissions within the group. |
| DeleteMembership | Membership | .google.protobuf.Empty | Leave a group (or cancel membership request). Authenticated. |
| GetPosts | GetPostsRequest | GetPostsResponse | Gets Posts. Publicly accessible or Authenticated. Unauthenticated calls only return Posts of GLOBAL_PUBLIC visibility. |
| CreatePost | Post | Post | Creates a Post. Authenticated. |
| UpdatePost | Post | Post | Updates a Post. Authenticated. |
| DeletePost | Post | Post | (TODO) (Soft) deletes a Post. Returns the deleted version of the Post. Authenticated. |
| StarPost | Post | Post | Star a Post. Unauthenticated. |
| UnstarPost | Post | Post | Unstar a Post. Unauthenticated. |
| SyncPost | SyncPostRequest | Post | Syncs (cross-posts) a Post to a SyncDestination. Authenticated (destination owner, or Admin), requires SYNC_POSTS_TO_FACEBOOK (or Admin). |
| DeletePostSyncDestination | DeletePostSyncDestinationRequest | .google.protobuf.Empty | Removes a Post's sync (cross-post) to a SyncDestination, the reverse of SyncPost. Authenticated (destination owner, or Admin), requires SYNC_POSTS_TO_FACEBOOK (or Admin). |
| GetGroupPosts | GetGroupPostsRequest | GetGroupPostsResponse | Get GroupPosts for a Post (and optional group). Publicly accessible or Authenticated. |
| CreateGroupPost | GroupPost | GroupPost | Cross-post a Post to a Group. Authenticated. |
| UpdateGroupPost | GroupPost | GroupPost | Group Moderators: Approve/Reject a GroupPost. Authenticated. |
| DeleteGroupPost | GroupPost | .google.protobuf.Empty | Delete a GroupPost. Authenticated. |
| GetEvents | GetEventsRequest | GetEventsResponse | Gets Events. Publicly accessible or Authenticated. Unauthenticated calls only return Events of GLOBAL_PUBLIC visibility. |
| CreateEvent | Event | Event | Creates an Event. Authenticated. |
| UpdateEvent | Event | Event | Updates an Event. Automatically creates/updates/deletes child Occasions of the Event. Authenticated. Since Events are more complex structures, UpdateEventDetails, CreateNewOccasions, UpdateOccasions, and DeleteRemovedOccasions are provided as separate RPCs to break down what happens during this request. |
| DeleteEvent | Event | Event | (Soft) deletes a Event. Returns the deleted version of the Event. Authenticated. |
| UpdateEventDetails | Event | Event | Updates only the Event's top-level details and those of its Post (not any Occasions or their Posts). Authenticated. |
| CreateNewOccasions | Event | Event | Creates Occasions in an existing Event for every Occasion in the request that isn't already on the event. Authenticated. Any other Occasions in the request are ignored. |
| UpdateOccasions | Event | Event | Updates Occasions in an existing Event for every Occasion in the request that's already on the event. Any other Occasions in the request are ignored. Authenticated. |
| DeleteRemovedOccasions | Event | Event | Deletes Occasions in an existing Event that aren't present in the input Event. Authenticated. |
| GetSyncSources | User | GetSyncSourcesResponse | Gets a user's SyncSources. Authenticated (self, or Admin for any user). |
| CreateSyncSource | SyncSource | SyncSource | Creates a SyncSource for the current user. Authenticated, requires SYNC_EVENTS_FROM_ICS/ SYNC_POSTS_FROM_RSS/SYNC_POSTS_FROM_ATOM (whichever matches configuration, or Admin). |
| UpdateSyncSource | SyncSource | SyncSource | Updates a SyncSource. Authenticated (owner, or Admin for any user's), requires SYNC_EVENTS_FROM_ICS/SYNC_POSTS_FROM_RSS/SYNC_POSTS_FROM_ATOM (whichever matches the effective configuration - the request's own if set, else the existing source's - or Admin). |
| DeleteSyncSource | DeleteSyncSourceRequest | .google.protobuf.Empty | Deletes a SyncSource. Authenticated (owner, or Admin). |
| GetSyncDestinations | User | GetSyncDestinationsResponse | Gets a user's SyncDestinations. Authenticated (self, or Admin for any user). |
| CreateSyncDestination | SyncDestination | SyncDestination | Creates a SyncDestination for the current user. Authenticated, requires SYNC_EVENTS_TO_FACEBOOK or SYNC_POSTS_TO_FACEBOOK (or Admin). |
| UpdateSyncDestination | SyncDestination | SyncDestination | Updates a SyncDestination. Authenticated (owner, or Admin for any user's), requires SYNC_EVENTS_TO_FACEBOOK or SYNC_POSTS_TO_FACEBOOK (or Admin). |
| DeleteSyncDestination | DeleteSyncDestinationRequest | .google.protobuf.Empty | Deletes a SyncDestination. Authenticated (owner, or Admin). |
| SyncOccasion | SyncOccasionRequest | Occasion | Syncs (cross-posts) an Occasion to a SyncDestination. Authenticated (destination owner, or Admin), requires SYNC_EVENTS_TO_FACEBOOK (or Admin). |
| DeleteOccasionSyncDestination | DeleteOccasionSyncDestinationRequest | .google.protobuf.Empty | Removes an Occasion's sync (cross-post) to a SyncDestination, the reverse of SyncOccasion. Authenticated (destination owner, or Admin), requires SYNC_EVENTS_TO_FACEBOOK (or Admin). |
| GetAIProviders | User | GetAIProvidersResponse | Gets a user's AIProviders. Authenticated (self, or Admin for any user). |
| CreateAIProvider | AIProvider | AIProvider | Creates an AIProvider for the current user. Authenticated, requires CREATE_AI_PROVIDERS (or Admin). |
| UpdateAIProvider | AIProvider | AIProvider | Updates an AIProvider's name, provider, or credentials. Authenticated (owner, or Admin for any user's). |
| DeleteAIProvider | DeleteAIProviderRequest | .google.protobuf.Empty | Deletes an AIProvider (and its AIProviderGrants). Authenticated (owner, or Admin). |
| GrantAIProvider | GrantAIProviderRequest | AIProviderGrant | Grants (or resets) another user's metered access to one of the current user's AIProviders. Authenticated, owner-only (no Admin override). |
| RevokeAIProvider | RevokeAIProviderRequest | .google.protobuf.Empty | Revokes another user's access to one of the current user's AIProviders. Authenticated, owner-only (no Admin override). |
| GetMarketProducts | GetMarketProductsRequest | GetMarketProductsResponse | Gets MarketProducts available for purchase on this server (market.proto). Unauthenticated -- admins additionally see delisted MarketProducts. |
| CreateMarketProduct | MarketProduct | MarketProduct | Creates a MarketProduct. Authenticated, requires Admin. |
| UpdateMarketProduct | MarketProduct | MarketProduct | Updates a MarketProduct's amount/currency/details/delisted_at. Authenticated, requires Admin. type/period are immutable after creation and are ignored if changed. |
| GetMarketSubscriptions | GetMarketSubscriptionsRequest | GetMarketSubscriptionsResponse | Gets MarketSubscriptions -- self-scoped ("MY subscriptions", GET_MARKET_SUBSCRIPTIONS_REQUEST_FOR_PURCHASE, the default) for any authenticated caller, or every PURCHASE_TYPE_RELLM_HOSTING subscription across every buyer (GET_MARKET_SUBSCRIPTIONS_REQUEST_FOR_FULFILLMENT_ADMIN, backing /market/fulfillment -- see RellmHostingSubscriptionDetails.fulfillment_status's own doc) for an Admin. Authenticated. |
| MakeMarketPurchase | MakeMarketPurchaseRequest | MakeMarketPurchaseResponse | Starts (or resumes) buying a MarketProduct for the current user, returning a Stripe Checkout URL to redirect to. Authenticated. See MakeMarketPurchaseRequest's own doc -- no MarketPurchase/ MarketSubscription is created by this call itself, only once Stripe confirms payment via webhook. |
| CancelMarketSubscription | MarketSubscription | MarketSubscription | Cancels a MarketSubscription by setting canceled_at to now -- entitlement (media storage quota, granted permissions, etc.) stays active until whichever is later of renews_at/canceled_at; the renew_market_subscriptions background job is what actually revokes it and sets service_terminated_at, once both have passed. Authenticated -- the subscription's own buyer, or an Admin. |
| UpdateMarketSubscription | MarketSubscription | MarketSubscription | Appends to a PURCHASE_TYPE_RELLM_HOSTING MarketSubscription's own fulfillment_notes (nothing else -- every other field, including additional_information, is immutable after purchase and silently ignored if changed) -- backs /market/fulfillment. A new entry must be appended (not inserted/reordered/removed) after whatever's already stored, and its user_id must match the caller's own -- the server stamps created_at itself, ignoring whatever the client sent. Its note text is required unless the entry also changes fulfillment_status from the previous entry's own value (see FulfillmentNote.note's own doc). fulfillment_status on the returned RellmHostingSubscriptionDetails is always the last appended entry's own value -- there's no way to set it independently of a note. Authenticated, requires Admin (for now -- see that field's own doc on why this may loosen to "buyer, or Admin" later). |
| GenerateMedia | GenerateMediaRequest | Media | Generates (or edits, given reference media_ids) an image via one of the current user's AIModels, storing it as a new Media and, if target is set, attaching it to that Post/Event. Authenticated - caller must own or have been granted access to the chosen AIProvider, and (if target is set) have edit access to that Post/Event. A grantee (never the provider's own owner) spends real AIProviderGrant.tokens_remaining on every call - the provider's own reported token usage once generation succeeds, or (rejected before any request is even sent to the provider) a rough pre-flight estimate of the request's input cost alone, whichever catches an insufficient balance first. |
| GetEventAttendances | GetEventAttendancesRequest | EventAttendances | Gets EventAttendances for an Occasion. Publicly accessible or Authenticated. |
| UpsertEventAttendance | EventAttendance | EventAttendance | Upsert an EventAttendance. Publicly accessible or Authenticated, with anonymous RSVP support. See EventAttendance and AnonymousAttendee for details. tl;dr: Anonymous RSVPs may updated/deleted with the AnonymousAttendee.auth_token returned by this RPC (the client should save this for the user, and ideally, offer a link with the token). |
| DeleteEventAttendance | EventAttendance | .google.protobuf.Empty | Delete an EventAttendance. Publicly accessible or Authenticated, with anonymous RSVP support. |
| FederateProfile | FederatedAccount | FederatedAccount | Federate the current user's profile with another user profile. Authenticated. |
| DefederateProfile | FederatedAccount | .google.protobuf.Empty | Authenticated*. |
| ConfigureServer | ServerConfiguration | ServerConfiguration | Configure the server (i.e. the response to GetServerConfiguration). Authenticated. Requires ADMIN permissions. Editing cluster_resources additionally requires EDIT_CLUSTER_SETTINGS - see that field's own doc. Editing supported_contact_protocols is validated, not just stored -- see ContactProtocol's own doc. |
| LockClusterResources | LockClusterResourcesRequest | LockClusterResourcesResponse | Attempts to acquire one or more ClusterResource locks on behalf of namespace_id. Not part of the authenticated-user auth system - this is server-to-server, cluster-internal coordination, authorized entirely by the cluster-shared-secret gRPC metadata header matching this server's own stored ClusterResources.cluster_shared_secret - knowing the secret is what makes a caller entitled to treat this server as the conductor, regardless of what this server's own ClusterResources.namespace_id/conductor_host happen to say. Fails with FAILED_PRECONDITION if this server has no cluster_resources configured at all (nothing to check the secret against), and with UNAUTHENTICATED if the header is missing or doesn't match. See LockClusterResourcesResponse for the polling contract this expects of callers. |
| FreeClusterResources | FreeClusterResourcesRequest | .google.protobuf.Empty | Releases resources previously acquired via LockClusterResources. Publicly accessible or Authenticated - unlike LockClusterResources, this accepts either the cluster-shared-secret header (same as LockClusterResources) or normal per-user auth, in which case the caller needs EDIT_CLUSTER_SETTINGS (see that permission's own doc) and the header is ignored entirely - this is what lets an admin free a stuck lock straight from the Cluster tab UI rather than needing shell access to the cluster's shared secret. See FreeClusterResourcesRequest's own doc for its no-op-if-not-held behavior. |
| ResetData | .google.protobuf.Empty | .google.protobuf.Empty | Delete ALL Media, Posts, Groups and Users except the user who performed the RPC. Authenticated. Requires ADMIN permissions. Note: Server Configuration is not deleted. |
| StreamReplies | Post | Post stream | (TODO) Reply streaming interface. Currently just streams fake example data. |
Request for a new access token using a refresh token.
| Field | Type | Label | Description |
|---|---|---|---|
| refresh_token | string | The refresh token to use to request a new access token. | |
| expires_at | google.protobuf.Timestamp | optional | Optional requested expiration time for the token. Server may ignore this. |
Returned when requesting access tokens.
| Field | Type | Label | Description |
|---|---|---|---|
| refresh_token | ExpirableToken | optional | If a refresh token is returned, it should be stored. Old refresh tokens may expire before their indicated expiration. See: https://auth0.com/docs/secure/tokens/refresh-tokens/refresh-token-rotation |
| access_token | ExpirableToken | The new access token. |
Request to create a new account.
| Field | Type | Label | Description |
|---|---|---|---|
| username | string | Username for the account to be created. Must not exist. | |
| password | string | Password for the account to be created. Must be at least 8 characters. | |
| ContactMethod | optional | Email to be used as a contact method. | |
| phone | ContactMethod | optional | Phone number to be used as a contact method. |
| expires_at | google.protobuf.Timestamp | optional | Request an expiration time for the Auth Token returned. By default it will not expire. |
| device_name | string | optional | (Not yet implemented.) The name of the device being used to create the account. |
Request to create a new third-party refresh token. Unlike LoginRequest or CreateAccountRequest, the user must be logged in to create a third-party refresh token.
Generally, this is used to create a refresh token for another Rellm instance,
e.g., accessing bullcity.social/jon's data from jonline.io. On the web side, this is implemented as follows:
bullcity.social user wants to login on jonline.io, bullcity.social will redirect
the user to jonline.io/third_party_auth?to=bullcity.social.
jonline.io will force the user to login if needed on this page.jonline.io will prompt/warn the user, and then call this RPC to create a refresh + access token for bullcity.social.jonline.io will redirect the user back to bullcity.social/third_party_auth?from=jonline.io&token=<Base64RefreshTokenResponse> with the refresh token POSTed in form data.
<Base64RefreshTokenResponse> is a base64-encoded RefreshTokenResponse message.)bullcity.social will ensure it can GetCurrentUser on jonline.io with its new auth token.bullcity.social will replace the current location with bullcity.social/third_party_auth?from=jonline.io.bullcity.social will use the access token to make requests to jonline.io (the same as with bullcity.social).Note that refresh tokens
| Field | Type | Label | Description |
|---|---|---|---|
| expires_at | google.protobuf.Timestamp | optional | The third-party refresh token's expiration time. |
| user_id | string | The third-party refresh token's user ID. | |
| device_name | string | The third-party refresh token's device name. |
Generic type for refresh and access tokens.
| Field | Type | Label | Description |
|---|---|---|---|
| token | string | The secure token value. | |
| expires_at | google.protobuf.Timestamp | optional | Optional expiration time for the token. If not set, the token will not expire. |
Request to login to an existing account.
| Field | Type | Label | Description |
|---|---|---|---|
| username | string | Username for the account to be logged into. Must exist. | |
| password | string | Password for the account to be logged into. | |
| expires_at | google.protobuf.Timestamp | optional | Request an expiration time for the Auth Token returned. By default it will not expire. |
| device_name | string | optional | (Not yet implemented.) The name of the device being used to login. |
| user_id | string | optional | (TODO) If provided, username is ignored and login is initiated via user_id instead. |
Metadata on a refresh token for the current user, used when managing refresh tokens as a user. Does not include the token itself.
| Field | Type | Label | Description |
|---|---|---|---|
| id | uint64 | The DB ID of the refresh token. Used when deleting the token or updating the device_name. | |
| expires_at | google.protobuf.Timestamp | optional | Expiration date of the refresh token. |
| device_name | string | optional | The device name the refresh token is on. User-updateable. |
| is_this_device | bool | Whether the refresh token is associated with the current device (based on what user is making the request). | |
| third_party | bool |
Returned when creating an account, logging in, or creating a third-party refresh token.
| Field | Type | Label | Description |
|---|---|---|---|
| refresh_token | ExpirableToken | The persisted token the device should store and associate with the account. Used to request new access tokens. | |
| access_token | ExpirableToken | An initial access token provided for convenience. | |
| user | User | The user associated with the account that was created/logged into. |
Request to reset a password.
| Field | Type | Label | Description |
|---|---|---|---|
| user_id | string | optional | If not set, use the current user of the request. |
| password | string | The new password to set. |
Response for GetUserRefreshTokens RPC. Returns all refresh tokens associated with the current user.
| Field | Type | Label | Description |
|---|---|---|---|
| refresh_tokens | RefreshTokenMetadata | repeated | The refresh tokens associated with the current user. |
Nearly everything in Rellm has one or more Moderations on it.
From a high level:
User has a moderation that determines whether they can log in (and their visibility per their visibility).
(This is poorly enforced currently! Fix it if you want!)
people_settings.default_moderation in ServerConfiguration.
A default of UNMODERATED means that all users can log in. A default of PENDING
means that all users must be approved by a moderator/admin before they can log in.
Follow has a target_user_moderation that determines whether the User is following the Group.
default_follow_moderation in the targeted User.Group has a moderation that determines whether the Group is visible to users (per its visibility).
group_settings.default_moderation in ServerConfiguration.Membership has a group_moderation and user_moderation that determine whether
the Group admins and/or the invited user has approved the Membership, respectively.
Post has a moderation that determines whether the Post is visible to users (per its visibility).
post_settings.default_moderation in ServerConfiguration.GroupPost has a moderation that determines whether the admins/mods of the Group has approved the Post (or Post-descended thing like Events).Events and further objects contain a Post and thus inherit its moderation and
related GroupPost behavior, for "Group Events."
| Name | Number | Description |
|---|---|---|
| MODERATION_UNKNOWN | 0 | A moderation that is not known to the protocol. (Likely, the client and server use different versions of the Rellm protocol.) |
| UNMODERATED | 1 | Subject has not been moderated and is visible to all users. |
| PENDING | 2 | Subject is awaiting moderation and not visible to any users. |
| APPROVED | 3 | Subject has been approved by moderators and is visible to all users. |
| REJECTED | 4 | Subject has been rejected by moderators and is not visible to any users. |
Visibility in Rellm is a complex topic. There are several different types of visibility,
and each type of entity (User, Media, Group, then Post/Event/etc. with common logic)
has different rules for visibility.
From the top down, the rules break down as follows:
PRIVATE entity is always visible to the user who owns it.
Groups, this means all full members of the Group.Users, this is confusing and there is a whole PrivateUserStrategy thing
in ServerConfiguration for this.
LIMITED entity is visible to to the owner(s) and any explicitly associated
Users and Groups. Generally, this only applies to Post/Event/etc. entities.
Associations exist via UserPosts and GroupPosts.
SERVER_PUBLIC entity is visible to all authenticated users.GLOBAL_PUBLIC entity is visible to the open internet.| Name | Number | Description |
|---|---|---|
| VISIBILITY_UNKNOWN | 0 | A visibility that is not known to the protocol. (Likely, the client and server use different versions of the Rellm protocol.) |
| PRIVATE | 1 | Subject is only visible to the user who owns it. |
| LIMITED | 2 | Subject is only visible to explictly associated Groups and Users. See: GroupPost and UserPost. |
| SERVER_PUBLIC | 3 | Subject is visible to all authenticated users. |
| GLOBAL_PUBLIC | 4 | Subject is visible to all users on the internet. |
| DIRECT | 5 | [TODO] Subject is visible to explicitly-associated Users. Only applicable to Posts and Events. For Users, this is the same as LIMITED. See: UserPost. |
Rellm Permissions are a set of permissions that can be granted directly to Users and Memberships.
(A Membership is the link between a Group and a User.)
Subsets of these permissions are also applicable to anonymous users via anonymous_user_permissions in ServerConfiguration,
and to Group non-members via non_member_permissions in Group, as well as others documented there.
| Name | Number | Description |
|---|---|---|
| PERMISSION_UNKNOWN | 0 | A permission that could not be read using the Rellm protocol. (Perhaps, a permission from a newer Rellm version.) |
| VIEW_USERS | 1 | Allow the user to view profiles with SERVER_PUBLIC Visibility. Allow anonymous users to view profiles with GLOBAL_PUBLIC Visibility (when configured as an anonymous user permission). |
| PUBLISH_USERS_LOCALLY | 2 | Allow the user to publish profiles with SERVER_PUBLIC Visibility. This generally only applies to the user's own profile, except for Admins. |
| PUBLISH_USERS_GLOBALLY | 3 | Allow the user to publish profiles with GLOBAL_PUBLIC Visibility. This generally only applies to the user's own profile, except for Admins. |
| MODERATE_USERS | 4 | Allow the user to grant VIEW_POSTS, CREATE_POSTS, VIEW_EVENTS and CREATE_EVENTS permissions to users. |
| FOLLOW_USERS | 5 | Allow the user to follow other users. |
| GRANT_BASIC_PERMISSIONS | 6 | Allow the user to grant Basic Permissions to other users. "Basic Permissions" are defined by your ServerConfiguration's basic_user_permissions. |
| VIEW_GROUPS | 10 | Allow the user to view groups with SERVER_PUBLIC visibility. Allow anonymous users to view groups with GLOBAL_PUBLIC visibility (when configured as an anonymous user permission). |
| CREATE_GROUPS | 11 | Allow the user to create groups. |
| PUBLISH_GROUPS_LOCALLY | 12 | Allow the user to give groups SERVER_PUBLIC visibility. |
| PUBLISH_GROUPS_GLOBALLY | 13 | Allow the user to give groups GLOBAL_PUBLIC visibility. |
| MODERATE_GROUPS | 14 | The Moderate Groups permission makes a user effectively an admin of any group. |
| JOIN_GROUPS | 15 | Allow the user to (potentially request to) join groups of SERVER_PUBLIC or higher visibility. |
| INVITE_GROUP_MEMBERS | 16 | Allow the user to invite other users to groups. Only applicable as a Group permission (not at the User level). |
| VIEW_POSTS | 20 | As a user permission, allow the user to view posts with SERVER_PUBLIC or higher visibility. As a group permission, allow the user to view GroupPosts whose Posts have LIMITED or higher visibility. Allow anonymous users to view posts with GLOBAL_PUBLIC visibility (when configured as an anonymous user permission). |
| CREATE_POSTS | 21 | As a user permission, allow the user to create Posts of PRIVATE and LIMITED visibility. As a group permission, allow the user to create GroupPosts for POST and FEDERATED_POST PostContexts.. |
| PUBLISH_POSTS_LOCALLY | 22 | Allow the user to publish posts with SERVER_PUBLIC visibility. |
| PUBLISH_POSTS_GLOBALLY | 23 | Allow the user to publish posts with GLOBAL_PUBLIC visibility. |
| MODERATE_POSTS | 24 | Allow the user to moderate posts. |
| REPLY_TO_POSTS | 25 | Allow the user to reply to posts. |
| EDIT_POST_TITLES_AND_LINKS | 26 | Allow the user to edit post titles and/or links. |
| VIEW_EVENTS | 30 | As a user permission, allow the user to view posts with SERVER_PUBLIC or higher visibility. As a group permission, allow the user to view GroupPosts whose Event Posts have LIMITED or higher visibility. Allow anonymous users to view events with GLOBAL_PUBLIC visibility (when configured as an anonymous user permission). |
| CREATE_EVENTS | 31 | As a user permission, allow the user to create Events of PRIVATE and LIMITED visibility. As a group permission, allow the user to create GroupPosts for EVENT and FEDERATED_OCCASION PostContexts.. |
| PUBLISH_EVENTS_LOCALLY | 32 | Allow the user to publish events with SERVER_PUBLIC visibility. |
| PUBLISH_EVENTS_GLOBALLY | 33 | Allow the user to publish events with GLOBAL_PUBLIC visibility. |
| MODERATE_EVENTS | 34 | Allow the user to moderate events. |
| RSVP_TO_EVENTS | 35 | Allow the user to RSVP to events that allow RSVPs. |
| VIEW_MEDIA | 40 | Allow the user to view media with SERVER_PUBLIC or higher visibility. Not currently enforced. Allow anonymous users to view media with GLOBAL_PUBLIC visibility (when configured as an anonymous user permission). Not currently enforced. |
| CREATE_MEDIA | 41 | Allow the user to create media of PRIVATE and LIMITED visibility. Not currently enforced. |
| PUBLISH_MEDIA_LOCALLY | 42 | Allow the user to publish media with SERVER_PUBLIC visibility. Not currently enforced. |
| PUBLISH_MEDIA_GLOBALLY | 43 | Allow the user to publish media with GLOBAL_PUBLIC visibility. Not currently enforced. |
| MODERATE_MEDIA | 44 | Allow the user to moderate events. |
| READ_PERSONAL_MESSAGES | 50 | |
| READ_ALL_SYSTEM_MESSAGES | 51 | |
| CREATE_AI_PROVIDERS | 60 | Allow the user to create/update their own AIProviders (see ai_providers.proto) and grant/revoke other users' access to them. |
| SYNC_EVENTS_FROM_ICS | 700 | Allow the user to create/update SyncSources (iCal subscriptions) that synchronize Events in. |
| SYNC_POSTS_FROM_RSS | 701 | Allow the user to create/update SyncSources (RSS subscriptions) that synchronize Posts in. |
| SYNC_POSTS_FROM_ATOM | 702 | Allow the user to create/update SyncSources (Atom subscriptions) that synchronize Posts in. |
| SYNC_EVENTS_TO_FACEBOOK | 1000 | Allow the user to create/update SyncDestinations that cross-post Occasions to a connected Facebook Page, and to sync Occasions to them. |
| SYNC_POSTS_TO_FACEBOOK | 1001 | Allow the user to create/update SyncDestinations that cross-post Posts to a connected Facebook Page, and to sync Posts to them. |
| SYNC_EVENTS_TO_INSTAGRAM | 1010 | Allow the user to create/update SyncDestinations that cross-post Occasions to a connected Instagram Business/Creator account, and to sync Occasions to them. |
| SYNC_POSTS_TO_INSTAGRAM | 1011 | Allow the user to create/update SyncDestinations that cross-post Posts to a connected Instagram Business/Creator account, and to sync Posts to them. |
| SYNC_EVENTS_TO_MASTODON | 1020 | Allow the user to create/update SyncDestinations that cross-post Occasions to a connected Mastodon account, and to sync Occasions to them. |
| SYNC_POSTS_TO_MASTODON | 1021 | Allow the user to create/update SyncDestinations that cross-post Posts to a connected Mastodon account, and to sync Posts to them. |
| SYNC_EVENTS_TO_BLUESKY | 1030 | Allow the user to create/update SyncDestinations that cross-post Occasions to a connected Bluesky account, and to sync Occasions to them. |
| SYNC_POSTS_TO_BLUESKY | 1031 | Allow the user to create/update SyncDestinations that cross-post Posts to a connected Bluesky account, and to sync Posts to them. |
| SYNC_EVENTS_TO_X_TWITTER | 1040 | Allow the user to create/update SyncDestinations that cross-post Occasions to a connected X (Twitter) account, and to sync Occasions to them. |
| SYNC_POSTS_TO_X_TWITTER | 1041 | Allow the user to create/update SyncDestinations that cross-post Posts to a connected X (Twitter) account, and to sync Posts to them. |
| SYNC_EVENTS_TO_THREADS | 1050 | Allow the user to create/update SyncDestinations that cross-post Occasions to a connected Threads account, and to sync Occasions to them. |
| SYNC_POSTS_TO_THREADS | 1051 | Allow the user to create/update SyncDestinations that cross-post Posts to a connected Threads account, and to sync Posts to them. |
| BUSINESS | 9998 | Indicates the user is a business. Used purely for display purposes. |
| RUN_BOTS | 9999 | Allow the user to run bots. There is no enforcement of this permission (yet), but it lets other users know that the user is allowed to run bots. |
| ADMIN | 10000 | Marks the user as an admin. In the context of user permissions, allows the user to configure the server, moderate/update visibility/permissions to any User, Group, Post or Event. In the context of group permissions, allows the user to configure the group, modify members and member permissions, and moderate GroupPosts and GroupEvents. |
| VIEW_PRIVATE_CONTACT_METHODS | 10001 | Allow the user to view the private contact methods of other users. Kept separate from ADMIN to allow for more fine-grained privacy control. |
| EDIT_CLUSTER_SETTINGS | 10002 | Allow the user to edit ServerConfiguration.cluster_resources via ConfigureServer. cluster_resources is otherwise visible (read-only) to any ADMIN - this permission gates editing it specifically, on top of ADMIN, since misconfiguring it (wrong conductor_host/cluster_shared_secret) affects cluster-mates this admin may not operate. Kept separate from ADMIN the same way VIEW_PRIVATE_CONTACT_METHODS is, and deliberately not grantable via UpdateUser like other permissions - only settable directly in the database (e.g. via the set_permission binary), so granting it is always a deliberate operator action, never a side effect of a normal admin-managing-admins flow. |
A single entry in a ContactMethod.consent_history, recording that its
consent_state became state as of changed_at. Our ultimate consent state is always the
state of the most recent (last) entry in consent_history --
ContactMethod.consent_state is just a denormalized copy of it, kept
for convenient access without walking the history.
| Field | Type | Label | Description |
|---|---|---|---|
| state | ContactConsentState | The ContactConsentState the ContactMethod was changed to. |
|
| changed_at | google.protobuf.Timestamp | When this change took effect. Always the server's own time as of the UpdateUser call that made the change -- any changed_at sent by a client is ignored. |
A contact method for a user (tel: or mailto:). SMS verification via
StartContactMethodVerification/
VerifyContactMethod, backed by whichever of
TwilioConfig/BirdConfig/
TelnyxConfig the server has enabled -- see supported_by_server below,
and ContactProtocol for the corresponding server-wide toggle.
mailto: has no verification provider yet.
| Field | Type | Label | Description |
|---|---|---|---|
| value | string | optional | Either a valid mailto: or valid tel: URL. |
| visibility | Visibility | The visibility of the contact method. | |
| supported_by_server | bool | Server-side flag indicating whether the server can verify (and otherwise interact via) the contact method. Always computed server-side (never trusted from client input) -- true iff this value's scheme (tel:/mailto:) is the corresponding ContactProtocol currently listed in ServerConfiguration.supported_contact_protocols (see the ServerConfiguration message). users.proto deliberately never imports server_configuration.proto (server configuration is kept abstracted from the rest of the protocol), so this relationship exists only in backend logic (contact_verification::contact_protocol_supported, called from update_user.rs) and in this doc comment, not as a formal schema reference. |
|
| verified_at | google.protobuf.Timestamp | optional | Time the contact method was verified. Indicates the user has completed verification of the contact method. Verification requires supported_by_server to be true, and is set by VerifyContactMethod on a correct code. |
| verification_in_progress | ContactMethodVerification | optional | Set while an SMS verification code has been sent and not yet confirmed, expired, or exhausted -- populated by StartContactMethodVerification and cleared by a successful VerifyContactMethod (which sets verified_at instead) or by expiry/too-many-attempts. See ContactMethodVerification for its own fields. |
| consent_state | ContactConsentState | Whether the user currently consents to being contacted via this ContactMethod (e.g. by SMS, for tel: values) by external services -- see docs/contact_integrations.md. External services (Twilio/Bird/Telnyx) may not contact the user unless this is CONTACT_CONSENT_GRANTED -- this includes StartContactMethodVerification's own outbound verification SMS, which fails with contact_consent_not_granted until consent is granted, even though the user is the one requesting the send. Defaults to CONTACT_CONSENT_REVOKED (proto3's zero value) so a ContactMethod with no explicit consent action is treated as not-consented. Settable via the UpdateUser RPC -- same self-or-ADMIN gate as value/ visibility (see update_user.rs's `admin |
|
| consent_history | ContactConsentChange | repeated | Append-only history of every consent_state change, oldest first. Not directly modifiable -- the server appends to it whenever UpdateUser changes consent_state, using the server's own time for ContactConsentChange.changed_at regardless of what the client sends. Same owner-or-ADMIN-only visibility as consent_state (blanked to empty for every other viewer). |
Encapsulates verification of a ContactMethod. Verification cannot
begin until contact consent (ContactMethod.consent_state) is granted -- see
StartContactMethodVerification.
| Field | Type | Label | Description |
|---|---|---|---|
| verification_code | string | Never serialized to gRPC by the backend. Only stored server-side; a client's own attempt to verify goes through VerifyContactMethodRequest.code instead, not this field. |
|
| verification_started_at | google.protobuf.Timestamp | ||
| attempts | int32 | Number of failed VerifyContactMethod attempts against verification_code since it was sent. Capped (see that RPC's own doc) to prevent brute-forcing the 6-digit code within its expiry window. |
Model for a user's follow of another user.
| Field | Type | Label | Description |
|---|---|---|---|
| user_id | string | The follower in the relationship. | |
| target_user_id | string | The user being followed. | |
| target_user_moderation | Moderation | Tracks whether the target user needs to approve the follow. | |
| created_at | google.protobuf.Timestamp | The time the follow was created. | |
| updated_at | google.protobuf.Timestamp | optional | The time the follow was last updated. |
Request to get one or more users by a variety of parameters.
Supported parameters depend on listing_type.
{listing_type: USERS_TEXT_SEARCH, search_text:}
{listing_type: FOLLOWERS_TEXT_SEARCH, search_text:, user_id:} (and the
FOLLOWING_TEXT_SEARCH/FRIENDS_TEXT_SEARCH/FOLLOW_REQUESTS_TEXT_SEARCH equivalents)
user_id's followers/following/friends/follow
requests, same relationship rules as the non-search listing_type.
| Field | Type | Label | Description |
|---|---|---|---|
| username | string | optional | The username to search for. Substrings are supported. |
| user_id | string | optional | The user ID to search for. |
| search_text | string | optional | Full-text search query, matched against the user's username/real name/bio. Required (and only used) when listing_type is USERS_TEXT_SEARCH or one of the *_TEXT_SEARCH variants. |
| page | int32 | optional | The page of results to return. Pages are 0-indexed. |
| listing_type | UserListingType | The number of results to return per page. |
Response to a GetUsersRequest.
| Field | Type | Label | Description |
|---|---|---|---|
| users | User | repeated | The users matching the request. |
| has_next_page | bool | Whether there are more pages of results. |
Model for a user's membership in a group. Memberships are generically included as part of User models when relevant in Rellm, but UIs should use the group_id to reconcile memberships with groups.
| Field | Type | Label | Description |
|---|---|---|---|
| user_id | string | The member (or requested/invited member). | |
| group_id | string | The group the membership pertains to. | |
| permissions | Permission | repeated | Valid Membership Permissions are: VIEW_POSTS, CREATE_POSTS, MODERATE_POSTS, VIEW_EVENTS, CREATE_EVENTS, MODERATE_EVENTS, ADMIN, RUN_BOTS, and MODERATE_USERS |
| group_moderation | Moderation | Tracks whether group moderators need to approve the membership. | |
| user_moderation | Moderation | Tracks whether the user needs to approve the membership. | |
| created_at | google.protobuf.Timestamp | The time the membership was created. | |
| updated_at | google.protobuf.Timestamp | optional | The time the membership was last updated. |
Model for a Rellm user. This user may have Media, Group Memberships,
Posts, Events, and other objects associated with them.
| Field | Type | Label | Description |
|---|---|---|---|
| id | string | Permanent string ID for the user. Will never contain a @ symbol. |
|
| username | string | Impermanent string username for the user. Will never contain a @ symbol. |
|
| real_name | string | The user's real name. | |
| ContactMethod | optional | The user's email address. | |
| phone | ContactMethod | optional | The user's phone number. |
| permissions | Permission | repeated | The user's permissions. See Permission for details. |
| avatar | MediaReference | optional | The user's avatar. Note that its visibility is managed by the User and thus it may not be accessible to the current user. |
| bio | string | The user's bio. | |
| media_storage_limit_bytes | uint64 | optional | The maximum number of bytes this user's Media (see Media.sizes[].size_bytes) may collectively occupy in storage. Enforced by POST /media (see Media's own doc), which rejects an upload that would push media_storage_bytes_used over this limit with an HTTP 413 and a plaintext error body. Unset means unlimited. |
| media_storage_bytes_used | uint64 | The total size, in bytes, of every stored copy (original plus any converted sizes) of every Media item this user owns -- the sum of Media.sizes[].size_bytes across all their Media. A denormalized counter, recomputed (never trusted from client input) after every operation that could change it -- upload, delete, size conversion, DeleteMediaSizes -- by backend/src/logic/user_counts.rs's update_media_storage_used, and self-healed on an interval by bin/update_user_counts.rs the same way every other denormalized User counter (follower_count, post_count, etc.) is. |
|
| visibility | Visibility | User visibility is a bit different from Post visibility. LIMITED means the user can only be seen by users they follow (as opposed to Posts' individualized visibilities). PRIVATE visibility means no one can see the user. See server_configuration.proto for details about PRIVATE users' ability to creep. | |
| moderation | Moderation | The user's moderation status. See Moderation for details. |
|
| default_follow_moderation | Moderation | Only PENDING or UNMODERATED are valid. | |
| follower_count | int32 | optional | The number of users following this user. |
| following_count | int32 | optional | The number of users this user is following. |
| friend_count | int32 | optional | The number of users this user mutually follows (and is followed by). |
| group_count | int32 | optional | The number of groups this user is a member of. |
| post_count | int32 | optional | The number of posts this user has made. |
| response_count | int32 | optional | The number of responses to Posts and Events this user has made. |
| event_count | int32 | optional | The number of events this user has created. |
| occasion_count | int32 | optional | The number of occasions this user has created (across all of their events). |
| current_user_follow | Follow | optional | Presence indicates the current user is following or has a pending follow request for this user. |
| target_current_user_follow | Follow | optional | Presence indicates this user is following or has a pending follow request for the current user. |
| current_group_membership | Membership | optional | Returned by GetMembers calls, for use when managing Group Memberships. The Membership should match the Group from the originating GetMembersRequest, providing whether the user is a member of that Group, has been invited, requested to join, etc.. |
| federated_profiles | FederatedAccount | repeated | Federated profiles for the user. Not always loaded. This is a list of profiles from other servers that the user has connected to their account. Managed by the user via Federate |
| sync_destinations | SyncDestination | repeated | The target user's own linked SyncDestinations (e.g. Facebook Pages). Populated by GetUsers' single-user lookups (by username or by user_id) when the viewer is the target user themselves (and holds SYNC_EVENTS_TO_FACEBOOK or SYNC_POSTS_TO_FACEBOOK) or an Admin, and by Login/CreateAccount/GetCurrentUser (always a self-view) - always empty otherwise, including via every other GetUsers listing type. |
| sync_sources | SyncSource | repeated | The target user's own SyncSources. Unlike sync_destinations, also populated for the target user themselves or an Admin across every GetUsers listing type (not just single-user lookups) - e.g. an Admin's EVERYONE listing gets every returned user's sources filled in, batch-loaded in one query rather than per-user. Also populated by Login/CreateAccount/GetCurrentUser (always a self-view). Always empty for any other viewer. |
| ai_models | AIModel | repeated | Every AIProvider model the target user may currently call - their own providers' models, plus any models granted to them on other users' providers (see AIModel). Gated and populated the same way as sync_sources (target user themselves, or an Admin, across any GetUsers listing type, plus Login/CreateAccount/GetCurrentUser). |
| market_subscriptions | MarketSubscription | repeated | The target user's own MarketSubscriptions (market.proto), each with its own billing_history. Gated and populated the same way as ai_models/sync_sources (target user themselves, or an Admin, across any GetUsers listing type, plus Login/CreateAccount/GetCurrentUser). |
| created_at | google.protobuf.Timestamp | The time the user was created. | |
| updated_at | google.protobuf.Timestamp | optional | The time the user was last updated. |
Request for VerifyContactMethod.
| Field | Type | Label | Description |
|---|---|---|---|
| value | string | The tel: (or, in the future, mailto:) value being verified -- must match the current user's own stored phone/email value. |
|
| code | string | The code the user was sent by StartContactMethodVerification. |
Whether a user has consented to being contacted (e.g. via SMS/email sent by external services)
through a given ContactMethod. See
ContactMethod.consent_state.
| Name | Number | Description |
|---|---|---|
| CONTACT_CONSENT_REVOKED | 0 | The user has not consented, or has revoked a prior consent. External services may not use this ContactMethod to contact the user. |
| CONTACT_CONSENT_GRANTED | 1 | The user currently consents to being contacted via this ContactMethod. |
Ways of listing users.
| Name | Number | Description |
|---|---|---|
| EVERYONE | 0 | Get all users. |
| FOLLOWING | 1 | Get users the current user is following. |
| FRIENDS | 2 | Get users who follow and are followed by the current user. |
| FOLLOWERS | 3 | Get users who follow the current user. |
| FOLLOW_REQUESTS | 4 | Get users who have requested to follow the current user. |
| USERS_TEXT_SEARCH | 5 | Returns users matching the full-text search_text query, scoped the same way EVERYONE is. Requires search_text parameter. |
Named USERS_TEXT_SEARCH (not the bare TEXT_SEARCH used by PostListingType) because proto3 enum values share a single namespace across the whole rellm package (C++ scoping rules) - PostListingType already claimed TEXT_SEARCH. |
| FOLLOWERS_TEXT_SEARCH | 6 | Scopes TEXT_SEARCH to users following user_id. Requires search_text and user_id. |
| FOLLOWING_TEXT_SEARCH | 7 | Scopes TEXT_SEARCH to users user_id follows. Requires search_text and user_id. |
| FRIENDS_TEXT_SEARCH | 8 | Scopes TEXT_SEARCH to user_id's friends (mutual follows). Requires search_text and user_id. |
| FOLLOW_REQUESTS_TEXT_SEARCH | 9 | Scopes TEXT_SEARCH to the signed-in caller's pending follow requests. Requires search_text. |
| ADMINS | 10 | [TODO] Gets admins for a server. |
Post/authorship-centric version of User. UI can cross-reference user details from its own cache (for things like admin/bot icons).
Lives in media.proto (rather than users.proto, where it used to live, or its own
authors.proto, split out from users.proto for a time) because Author.avatar needs
MediaReference and Media/MediaReference need Author (see this field's own doc) --
mutually recursive types belong in the same file, since protoc rejects circular file
imports even though the recursive types themselves are perfectly valid. users.proto
(User.sync_destinations) and sync.proto (SyncDestination.owner, SyncSource.owner) both
depend on this without depending on each other, via their own import "media.proto" (both
already needed it anyway, for User.avatar/Media-shaped fields).
| Field | Type | Label | Description |
|---|---|---|---|
| user_id | string | Permanent string ID for the user. Will never contain a @ symbol. |
|
| username | string | optional | Impermanent string username for the user. Will never contain a @ symbol. |
| avatar | MediaReference | optional | The user's avatar. |
| real_name | string | optional | |
| permissions | Permission | repeated |
Valid GetMediaRequest formats:
{user_id: abc123} - Gets the media of the given user that the current user can see. IE:
GLOBAL_PUBLIC media for the user if the current user is not logged in.SERVER_PUBLIC media for the user if the current user is logged in.LIMITED media for the user if the current user is following the user.{media_id: abc123} - Gets the media with the given ID, if visible to the current user.| Field | Type | Label | Description |
|---|---|---|---|
| media_id | string | optional | Returns the single media item with the given ID. |
| user_id | string | optional | Returns all media items for the given user. |
| page | uint32 |
| Field | Type | Label | Description |
|---|---|---|---|
| media | Media | repeated | |
| has_next_page | bool |
A Rellm Media message represents a single media item, such as a photo or video.
Media data is deliberately not accessible from the gRPC API. Instead, the client
should fetch media from http[s]://my.rellm.instance/media/{id}, unless url is set,
in which case that URL should be used instead (used for media Rellm doesn't store
locally, e.g. from federated ActivityPub/Mastodon or AT Protocol/Bluesky content).
Media items may be created with a HTTP POST to http[s]://my.rellm.instance/media
along with an "Authorization" header (your access token) and a "Content-Type" header.
On success, the endpoint will return the media ID in plaintext.
POST /media supports the following headers:
Content-Type - The MIME content type of the media item.Filename - An optional title for the media item.Authorization - Rellm Access Token for the user. Required, but may be supplied in Cookies.Cookies - Standard web cookies. The rellm_access_token cookie may be used for authentication.GET /media/{id} supports the following:
Authorization - Rellm Access Token for the user. May also be supplied in Cookies or via query parameter.Cookies - Standard web cookies. The rellm_access_token cookie may be used for authentication.authorization - Rellm Access Token for the user. May also be supplied in the Cookies or Authorization headers.GLOBAL_PUBLIC visibility.| Field | Type | Label | Description |
|---|---|---|---|
| id | string | The ID of the media item. | |
| author | Author | optional | The user who created the media item. |
| name | string | optional | An optional title for the media item. |
| description | string | optional | An optional description for the media item. |
| visibility | Visibility | Visibility of the media item. | |
| moderation | Moderation | Moderation of the media item. | |
| generated | bool | Indicates the media was generated by the server rather than uploaded manually by a user. | |
| processed | bool | Media is generally stored as-is on upload. When background jobs process and compress the media, this flag is set to true. | |
| created_at | google.protobuf.Timestamp | ||
| updated_at | google.protobuf.Timestamp | ||
| metadata | MediaMetadata | ||
| url | string | optional | An external URL to fetch the media from, in lieu of /media/{id}. Used for representing media owned by other protocols/servers (e.g. ActivityPub/Mastodon, AT Protocol/Bluesky) that Rellm does not store locally. If unset, clients fall back to /media/{id}. |
| sizes | MediaSize | repeated | Every stored copy of this media item's bytes -- the original upload (MEDIA_CONVERSION_ORIGINAL) plus any auto-generated resized copies (see convert_media_sizes's background job) -- each with its own content type, byte size, and (once known) aspect ratio. Always has at least one MEDIA_CONVERSION_ORIGINAL entry unless url is set (externally-hosted media has no locally-stored copies at all). The original is tracked here rather than as a separate top-level field so it can eventually be deleted to free space once converted copies exist, while remaining fully accounted for by User.media_storage_bytes_used up until that point. |
Free-form metadata about a Media item that isn't queried/filtered on, so doesn't need its
own columns.
| Field | Type | Label | Description |
|---|---|---|---|
| video_preview_time_ms | uint64 | optional | For video media, how far into the video (in milliseconds) its preview/poster frame should be taken from -- both via a #t=<seconds> Media Fragments URI on the <video> element's src, and as the timestamp ffmpeg seeks to when generating the VIDEO_PREVIEW_THUMBNAIL_* poster frames (see MediaConversion). Unset defaults to 1s (1000 in ms), or if the video is shorter than 1.5s, the midpoint of the video. Settable via UpdateMedia; changing it invalidates (deletes) any existing VIDEO_PREVIEW_THUMBNAIL_* sizes, so the convert_media_sizes background job regenerates them at the new time. |
A reference to a media item, designed to be included in other messages as a reference. Contains the bare minimum data needed to fetch media via the HTTP API and render it, and the media item's name (for alt text usage).
| Field | Type | Label | Description |
|---|---|---|---|
| id | string | The ID of the media item. | |
| name | string | optional | An optional title for the media item. |
| generated | bool | Indicates the media was generated by the server rather than uploaded manually by a user. | |
| metadata | MediaMetadata | ||
| sizes | MediaSize | repeated | See Media.sizes. |
| url | string | optional | An external URL to fetch the media from, in lieu of /media/{id}. See Media.url. If unset, clients fall back to /media/{id}. |
| description | string | optional | |
| author | Author | optional | The user who created the media item. See Media.author. Included here (unlike most other MediaReference fields, which are deliberately pared down from Media) so clients that only ever see a MediaReference -- e.g. a Post.media item -- can still tell whether the current viewer owns it, without a separate Media lookup. |
One stored copy of a Media item's bytes -- either its untouched original upload
(MEDIA_CONVERSION_ORIGINAL) or an auto-generated resized copy, as produced by the
convert_media_sizes background job. Fields are tracked per-size (rather than once on Media
itself) so that a future conversion producing a different kind of derived copy -- e.g. a
image/jpeg poster frame for a video/mp4 original, or a differently-cropped aspect ratio --
can vary any of them independently of the original.
| Field | Type | Label | Description |
|---|---|---|---|
| conversion | MediaConversion | Which copy this is -- the untouched original, or one of the auto-generated resized copies. | |
| size_bytes | uint64 | This copy's size on disk/in MinIO, in bytes. Summed (across every size, of every Media a user owns) into User.media_storage_bytes_used. |
|
| aspect_ratio | float | optional | Width divided by height. Set by the convert_media_sizes background job once it's able to read the media's dimensions (via ImageMagick/ffprobe); unset until then. |
| content_type | string | The MIME content type of this copy specifically. Usually identical across every size of a given Media, but not guaranteed to be -- e.g. a future video-thumbnail conversion could produce an image/jpeg size for a video/mp4 original. |
Which stored copy of a Media item's bytes a MediaSize represents.
| Name | Number | Description |
|---|---|---|
| MEDIA_CONVERSION_ORIGINAL | 0 | The untouched original upload. |
| MEDIA_CONVERSION_SMALL | 1 | Resized to fit within 320x320 px, preserving aspect ratio (never upscaled). |
| MEDIA_CONVERSION_MEDIUM | 2 | Resized to fit within 800x800 px, preserving aspect ratio (never upscaled). |
| MEDIA_CONVERSION_LARGE | 3 | Resized to fit within 1600x1600 px, preserving aspect ratio (never upscaled). |
| VIDEO_PREVIEW_THUMBNAIL_SMALL | 5 | For video media only: a image/jpeg poster frame captured (via ffmpeg) at MediaMetadata.video_preview_time_ms, resized to fit within 320x320 px, preserving aspect ratio (never upscaled) -- same dimension tier as MEDIA_CONVERSION_SMALL, just a still frame of the video rather than a resized copy of it. Regenerated by the same convert_media_sizes background job; deleted (and marked for regeneration) whenever UpdateMedia changes video_preview_time_ms, since the existing frame no longer matches the requested time. |
| VIDEO_PREVIEW_THUMBNAIL_MEDIUM | 6 | Same as VIDEO_PREVIEW_THUMBNAIL_SMALL, but resized to fit within 800x800 px -- the MEDIA_CONVERSION_MEDIUM tier. |
| VIDEO_PREVIEW_THUMBNAIL_LARGE | 7 | Same as VIDEO_PREVIEW_THUMBNAIL_SMALL, but resized to fit within 1600x1600 px -- the MEDIA_CONVERSION_LARGE tier. |
Request to get messages from the server. The request may be filtered by message ID, search text, or creation time. All non-text-search requests return messages in reverse chronological order (newest first). Text search requests return messages in order of relevance to the search text.
| Field | Type | Label | Description |
|---|---|---|---|
| listing_type | MessageListingType | The type of message listing to return. Required. | |
| message_id | string | optional | Returns the single message with the given ID (assuming the user has access to it). |
| message_group_id | string | optional | Returns messages that are part of the given messaging group (assuming the user has access to it). |
| search_text | string | optional | Full-text search query, matched against the sender's username/real name and the message's subject and body. Required (and only used) when listing_type is TEXT_SEARCH. |
| sent_before | google.protobuf.Timestamp | optional | Request to only return posts that were published or created before the given timestamp. |
| from_email | string | optional | Returns messages (assuming the user has access to each) whose email "from" header exactly matches the given value - i.e. Message.from as returned by a previous response. Meant for expanding the "sender" grouping a client falls back to when Message.messaging_group isn't set (see that field's own doc comment): unlike message_group_id, there's no server-side group backing this, so it's just a straight filter, not an access-controlled entity lookup. Since from is unauthenticated/spoofable (see this file's own top-level doc comment), so is this filter - it matches whatever string the sender's email client sent, nothing more. |
Response to a GetMessagesRequest, containing the requested messages.
| Field | Type | Label | Description |
|---|---|---|---|
| messages | Message | repeated | The messages that match the request. May be empty if no messages match. May be shortened to a server-defined limit, dependent on service version, configuration, load, etc. |
Checks whether the current user has already registered a given Web Push subscription endpoint.
See GetPushSubscriptionStatus's own RPC doc comment.
| Field | Type | Label | Description |
|---|---|---|---|
| endpoint | string | The Web Push subscription endpoint URL to check, as given by PushManager.subscribe(). |
| Field | Type | Label | Description |
|---|---|---|---|
| registered | bool | Whether the current user has a PushSubscription registered for this exact endpoint. |
Marks (or unmarks) one or more Messages as read by the calling user, e.g. every message in a
thread once it's been opened. Authenticated - read status is inherently personal, so there's
no anonymous variant the way SendMessage has one.
| Field | Type | Label | Description |
|---|---|---|---|
| unread | bool | If false (the default), the request is to mark the messages as read. If true, marks them (back) as unread instead - e.g. an explicit "mark unread" action on an already-read message. |
|
| message_ids | string | repeated | The Messages to mark read/unread. The caller must have the same access to each of them GetMessages would require (sender, a messaging_group member, a Bcc recipient, or an admin) - see MarkMessagesRead's own RPC doc comment. A message id the caller doesn't have access to fails the whole request (see that RPC's own doc on atomicity) rather than silently skipping it. |
Response to a MarkMessagesReadRequest - one MessageRead per message_ids entry, in the
same order, each reflecting that message's own read/unread result (see MarkMessagesReadRequest.unread).
| Field | Type | Label | Description |
|---|---|---|---|
| message_reads | MessageRead | repeated |
A Rellm Message represents a single message/email sent to one or more recipients
(really, "zero or more", as the design incorporates undeliverable messages).
| Field | Type | Label | Description |
|---|---|---|---|
| id | string | The ID of the message. | |
| sender | Author | optional | The sender of the message. Note that this is purported (we don't protect against spoofing). |
| messaging_group | MessagingGroup | optional | Note that, on the backend, every message actually has a messaging group. From the client's perspective, if messaging_group is not set, you were BCC'ed on the message and don't have access to the messaging group. |
| body_text | string | The body text of the message. For email messages, this is the email body. | |
| subject | string | optional | Subject of the message. For email messages, this is the email subject. |
| email_message_id | string | optional | If this message derived from an email, the original email's message ID (RFC 5322). Used to prevent duplicate messages from being created when the same email is sent multiple times. |
| from | string | optional | If this message derived from an email, the original email's "from" address. |
| to | string | optional | If this message derived from an email, the original email's "to" address. |
| cc | string | optional | If this message derived from an email, the original email's "cc" address. |
| bcc | string | optional | If this message derived from an email, the original email's "bcc" address. |
| current_user_read | MessageRead | optional | Whether/when this response's viewer has read the message - unset means unread. Always reflects the currently-authenticated caller's own read status (via MarkMessagesRead), even when browsing ALL_SYSTEM_MESSAGES(_TEXT_SEARCH) as an admin: it's a personal "have I seen this" marker, not tied to whichever user this response happens to be showing messaging_group for. |
| created_at | google.protobuf.Timestamp | The time the message was created. |
Records that a user has read a particular Message - one row (conceptually; see the composite
message_id/user_id key on the backing table) per (Message, user) that's ever been marked
read. Only ever surfaced back to the user it belongs to, as Message.current_user_read - there's
no RPC to see other users' read status on a Message.
| Field | Type | Label | Description |
|---|---|---|---|
| message_id | string | ||
| user_id | string | ||
| read_at | google.protobuf.Timestamp | When the message was marked read. Always set on a MessageRead returned from MarkMessagesRead - including a { unread: true } call, where it's simply the time of that unmark request, not a meaningful "last read" timestamp (there's no longer a row for it to come from at that point). |
A group of users who are participating in a conversation.
Most servers will probably have a (dynamically created) "empty group" for an email like
not_a_user@my_rellm_instance.com.
| Field | Type | Label | Description |
|---|---|---|---|
| id | string | The ID of the messaging group. | |
| members | Author | repeated | The users who are members of the group. Note that this is a superset of the users who are |
| created_at | google.protobuf.Timestamp | The time the group was created. |
A browser's Web Push subscription (see https://developer.mozilla.org/en-US/docs/Web/API/Push_API), registered so the server can push new-Message notifications to it even while the browser tab is closed. Only ever surfaced back to the user who registered it - there's no RPC to list other users' subscriptions.
| Field | Type | Label | Description |
|---|---|---|---|
| id | string | The ID of the subscription. | |
| endpoint | string | The Web Push subscription endpoint URL, as given by PushManager.subscribe(). |
|
| created_at | google.protobuf.Timestamp | The time the subscription was registered. |
Registers (or re-registers) a browser's Web Push subscription for the current user, so new
Messages sent/delivered to them push a notification even while the browser tab is closed.
See RegisterPushSubscription's own RPC doc comment.
| Field | Type | Label | Description |
|---|---|---|---|
| endpoint | string | The Web Push subscription endpoint URL, as given by PushManager.subscribe(). |
|
| p256dh_key | string | The subscription's p256dh key (base64url), as given by PushSubscription.getKey('p256dh'). |
|
| auth_key | string | The subscription's auth key (base64url), as given by PushSubscription.getKey('auth'). |
Request to create a new message. The server will create a new messaging group for the message, and send it to the given recipients.
| Field | Type | Label | Description |
|---|---|---|---|
| to_user_ids | string | repeated | |
| subject | string | optional | |
| body_text | string | optional |
Unregisters a browser's Web Push subscription for the current user, e.g. on logout or when
PushManager.subscribe() reports the subscription as no longer valid. See
UnregisterPushSubscription's own RPC doc comment.
| Field | Type | Label | Description |
|---|---|---|---|
| endpoint | string | The Web Push subscription endpoint URL to unregister, as previously passed to RegisterPushSubscription. |
| Name | Number | Description |
|---|---|---|
| PERSONAL_MESSAGES | 0 | Gets messages sent to the current user, and messages (purportedly) sent by the user. |
| PERSONAL_MESSAGES_TEXT_SEARCH | 1 | Gets messages sent to the current user, and messages (purportedly) sent by the user, that match the given search text. Returns results in order of relevance to the search text. |
| ALL_SYSTEM_MESSAGES | 10 | Gets all messages on the server (to a limit), including those sent to other users. Requires admin privileges. |
| ALL_SYSTEM_MESSAGES_TEXT_SEARCH | 11 |
Removes a single Post's sync (cross-post) to one SyncDestination - the reverse of SyncPost.
Does not delete the post already made on the destination (e.g. the Facebook Page post), only the local sync record.
| Field | Type | Label | Description |
|---|---|---|---|
| post_id | string | The Post to un-sync. | |
| sync_destination_id | string | The SyncDestination to un-sync it from. |
Used for getting context about GroupPosts of an existing Post.
| Field | Type | Label | Description |
|---|---|---|---|
| post_id | string | The ID of the post to get GroupPosts for. |
|
| group_id | string | optional | The ID of the group to get GroupPosts for. |
Used for getting context about GroupPosts of an existing Post.
| Field | Type | Label | Description |
|---|---|---|---|
| group_posts | GroupPost | repeated | The GroupPosts for the given Post or Group. |
Valid GetPostsRequest formats:
{[listing_type: AllAccessiblePosts]}
{listing_type:MyGroupsPosts|FollowingPosts}
{post_id:}
{post_id:, reply_depth: 1}
{listing_type: MyGroupsPosts|[GroupPost](#rellm-GroupPost)sPendingModeration, group_id:}
{author_user_id:, group_id:}
{listing_type: AuthorPosts, author_user_id:}
{listing_type: TextSearch, search_text:}
{listing_type: TextSearch, search_text:, author_user_id:} scopes the search to one author.| Field | Type | Label | Description |
|---|---|---|---|
| post_id | string | optional | Returns the single post with the given ID. |
| author_user_id | string | optional | Limits results to those by the given author user ID. |
| group_id | string | optional | Limits results to those in the given group ID. |
| reply_depth | uint32 | optional | Only supported for depth=2 for now. |
| context | PostContext | optional | Only POST and REPLY are supported for now. |
| post_ids | string | optional | Returns expanded posts with the given IDs. |
| listing_type | PostListingType | The listing type of the request. See PostListingType for more info. |
|
| page | uint32 | The page of results to return. Defaults to 0. | |
| search_text | string | optional | Full-text search query, matched against the author's username/real name and the post's title/link/content. Required (and only used) when listing_type is TEXT_SEARCH. |
| published_or_created_before | google.protobuf.Timestamp | optional | Request to only return posts that were published or created before the given timestamp. |
Used for getting posts.
| Field | Type | Label | Description |
|---|---|---|---|
| posts | Post | repeated | The posts returned by the request. |
A GroupPost is a cross-post of a Post to a Group. It contains
information about the moderation of the post in the group, as well as
the time it was cross-posted and the user who did the cross-posting.
| Field | Type | Label | Description |
|---|---|---|---|
| group_id | string | The ID of the group this post is in. | |
| post_id | string | The ID of the post. | |
| user_id | string | Deprecated. Deprecated.** Prefer to use shared_by. The ID of the user who cross-posted the post. |
|
| group_moderation | Moderation | The moderation of the post in the group. | |
| created_at | google.protobuf.Timestamp | The time the post was cross-posted. | |
| shared_by | Author | Author info for the user who cross-posted the post. |
A Post is a message that can be posted to the server. Its visibility
as well as any associated GroupPosts and UserPosts determine what users
see it and where.
Posts are also a fundamental unit of the system. They provide a building block
of Visibility and Moderation management that is used throughout Posts, Replies, Events,
and Occasions.
| Field | Type | Label | Description |
|---|---|---|---|
| id | string | Unique ID of the post. | |
| author | Author | optional | The author of the post. This is a smaller version of User. |
| reply_to_post_id | string | optional | If this is a reply, this is the ID of the post it's replying to. |
| title | string | optional | The title of the post. This is invalid for replies. |
| link | string | optional | The link of the post. This is invalid for replies. |
| content | string | optional | The content of the post. This is required for replies. |
| response_count | int32 | The number of responses (replies and replies to replies, etc.) to this post. | |
| reply_count | int32 | The number of direct replies to this post. | |
| group_count | int32 | The number of groups this post is in. | |
| media | MediaReference | repeated | List of Media IDs associated with this post. Order is preserved. |
| media_generated | bool | Flag indicating whether Media has been generated for this Post. Currently previews are generated for any Link post. | |
| embed_link | bool | Flag indicating | |
| shareable | bool | Flag indicating a LIMITED or SERVER_PUBLIC post can be shared with groups and individuals, and a DIRECT post can be shared with individuals. |
|
| context | PostContext | Context of the Post (POST, REPLY, EVENT, or OCCASION.) |
|
| visibility | Visibility | The visibility of the Post. | |
| moderation | Moderation | The moderation of the Post. | |
| post_media_layout | PostMediaLayout | The desired end-user layout of Media attached to the post. | |
| current_group_post | GroupPost | optional | If the Post was retrieved from GetPosts with a group_id, the GroupPost metadata may be returned along with the Post. |
| replies | Post | repeated | Hierarchical replies to this post. There will never be more than reply_count replies. However, there may be fewer than reply_count replies if some replies are hidden by moderation or visibility. Replies are not generally loaded by default, but can be added to Posts in the frontend. |
| created_at | google.protobuf.Timestamp | The time the post was created. | |
| updated_at | google.protobuf.Timestamp | optional | The time the post was last updated. |
| published_at | google.protobuf.Timestamp | optional | The time the post was published (its visibility first changed to SERVER_PUBLIC or GLOBAL_PUBLIC). |
| last_activity_at | google.protobuf.Timestamp | The time the post was last interacted with (replied to, etc.) | |
| unauthenticated_star_count | int64 | The number of unauthenticated stars on the post. | |
| sync_destinations | SyncDestinationStatus | repeated | SyncDestinations this post has been synced (cross-posted) to, and their status. |
| sync_source | SyncSource | optional | If the Post was created/is kept in sync from a SyncSource (an ICS Event/Occasion, or an RSS/Atom feed item), this is the source it was synced from. Only its media should be considered editable for such a Post. |
Syncs (cross-posts) a single Post to one SyncDestination.
| Field | Type | Label | Description |
|---|---|---|---|
| post_id | string | The Post to sync. | |
| sync_destination_id | string | The SyncDestination to sync it to. |
A UserPost is a "direct share" of a Post to a User. Currently unused/unimplemented.
See also: DIRECT Visibility.
| Field | Type | Label | Description |
|---|---|---|---|
| user_id | string | The ID of the user the post is shared with. | |
| post_id | string | The ID of the post shared. | |
| created_at | google.protobuf.Timestamp | The time the post was shared. |
Differentiates the context of a Post, as in Rellm's data models, Post is the "core" type where Rellm consolidates moderation and visibility data and logic.
| Name | Number | Description |
|---|---|---|
| POST | 0 | "Standard" or "Top-Level" Post. Can have media, a link, a title, and/or content. If provided, its link and title are permanent. |
| REPLY | 1 | Reply to a POST, REPLY, EVENT, or OCCASION Does not support a link. Requires a reply_to_post_id. |
| EVENT | 2 | Post behind an "Event" (which does not actually have a start/end time - it's a group of Occasions, at least one, which each do). The Events table should have a row for this Post. Never created by the CreatePost RPC (this is an error); use CreateEvent. These Posts' link and title fields are modifiable. |
| OCCASION | 3 | An "Occasion" Post (which relates to an event with a start and end time). The Occasions table should have a row for this Post. Never created by the CreatePost RPC (this is an error); use CreateEvent/UpdateEvent to manage Occasions implicitly. These Posts' link and title fields are modifiable. |
| FEDERATED_REPLY | 10 | A reply to a Post on another server. The post must have a link of the format http[s]://<server/post/<post_id> in its link field. It will not have a reply_to_post_id value. |
A high-level enumeration of general ways of requesting posts.
| Name | Number | Description |
|---|---|---|
| ALL_ACCESSIBLE_POSTS | 0 | Gets SERVER_PUBLIC and GLOBAL_PUBLIC posts as is sensible. Also usable for getting replies anywhere. |
| FOLLOWING_POSTS | 1 | Returns posts from users the user is following. |
| MY_GROUPS_POSTS | 2 | Returns posts from any group the user is a member of. |
| DIRECT_POSTS | 3 | Returns DIRECT posts that are directly addressed to the user. |
| POSTS_PENDING_MODERATION | 4 | Returns posts pending moderation by the server-level mods/admins. |
| TEXT_SEARCH | 5 | Returns posts matching the full-text search_text query, scoped the same way ALL_ACCESSIBLE_POSTS is (plus author_user_id, if provided). Requires search_text parameter. |
| GROUP_POSTS | 10 | Returns posts from a specific group. Requires group_id parameter. |
| GROUP_POSTS_PENDING_MODERATION | 11 | Returns pending_moderation posts from a specific group. Requires group_id parameter and user must have group (or server) admin permissions. |
| Name | Number | Description |
|---|---|---|
| MEDIA_LAYOUT_STANDARD | 0 | |
| MEDIA_LAYOUT_DYNAMIC_VERTICAL_SCROLL | 1 |
An anonymous internet user who has RSVP'd to an Occasion.
(TODO:) The visibility on AnonymousAttendee ContactMethods should support the LIMITED visibility, which will
make them visible to the event creator.
| Field | Type | Label | Description |
|---|---|---|---|
| name | string | A name for the anonymous user. For instance, "Bob Gomez" or "The guy on your front porch." | |
| contact_methods | ContactMethod | repeated | Contact methods for anonymous attendees. Currently not linked to Contact methods for users. |
| auth_token | string | optional | Used to allow anonymous users to RSVP to an event. Generated by the server when an event attendance is upserted for the first time. Subsequent attendance upserts, with the same occasion_id and anonymous_attendee.auth_token, will update existing anonymous attendance records. Invalid auth tokens used during upserts will always create a new EventAttendance. |
Removes a single Occasion's sync (cross-post) to one SyncDestination - the reverse of SyncOccasion.
Does not delete the post already made on the destination (e.g. the Facebook Page post), only the local sync record.
| Field | Type | Label | Description |
|---|---|---|---|
| occasion_id | string | The Occasion to un-sync. | |
| sync_destination_id | string | The SyncDestination to un-sync it from. |
An Event is a top-level type used to organize calendar events, RSVPs, and messaging/posting
about the Event. Actual time data lies in its Occasions.
(Eventually, Rellm Events should also support ticketing.)
| Field | Type | Label | Description |
|---|---|---|---|
| post | Post | The Post containing the underlying data for the event (title, content, moderation, visibility, etc.). Its PostContext should be EVENT. An Event's ID is its post.id - there is no separate surrogate ID. |
|
| info | EventInfo | Event configuration like whether to allow (anonymous) RSVPs, etc. | |
| occasions | Occasion | repeated | A list of occasions for the Event. Events will only include all occasions if the request is for a single event. |
Could be called an "RSVP." Describes the attendance of a user at an Occasion. Such as:
Occasion (one of INTERESTED, GOING, NOT_GOING, or , REQUESTED (i.e. invited)).Occasion.ContactMethod-driven management for anonymous RSVPs to an Occasion.| Field | Type | Label | Description |
|---|---|---|---|
| id | string | Unique server-generated ID for the attendance. | |
| occasion_id | string | ID of the Occasion the attendance is for. |
|
| user_attendee | UserAttendee | If the attendance is non-anonymous, core data about the user. | |
| anonymous_attendee | AnonymousAttendee | If the attendance is anonymous, core data about the anonymous attendee. | |
| number_of_guests | uint32 | Number of guests including the RSVPing user. (Minimum 1). | |
| status | AttendanceStatus | The user's RSVP to an Occasion (one of INTERESTED, REQUESTED (i.e. invited), GOING, NOT_GOING) |
|
| inviting_user_id | string | optional | User who invited the attendee. (Not yet used.) |
| private_note | string | Public note for everyone who can see the event to see. | |
| public_note | string | Private note for the event owner. | |
| moderation | Moderation | Moderation status for the attendance. Moderated by the Event owner (or Occasion owner if applicable). |
|
| created_at | google.protobuf.Timestamp | The time the attendance was created. | |
| updated_at | google.protobuf.Timestamp | optional | The time the attendance was last updated. |
Response to get RSVP data for an event.
| Field | Type | Label | Description |
|---|---|---|---|
| attendances | EventAttendance | repeated | The attendance data for the event, in no particular order. |
| hidden_location | Location | optional | When hide_location_until_rsvp_approved is set, the location of the event. |
To be used for ticketing, RSVPs, etc. Stored as JSON in the database.
| Field | Type | Label | Description |
|---|---|---|---|
| allows_rsvps | bool | optional | Whether to allow RSVPs for the event. |
| allows_anonymous_rsvps | bool | optional | Whether to allow anonymous RSVPs for the event. |
| max_attendees | uint32 | optional | Limit the max number of attendees. No effect unless allows_rsvps is true. Not yet supported. |
| hide_location_until_rsvp_approved | bool | optional | Hide the location until the user RSVPs (and it's accepted). From a system perspective, when this is set, Events will not include the Location until the user has RSVP'd. Location will always be returned in EventAttendances if the request for the EventAttendances came from a (logged in or anonymous) user whose attendance is approved (or the event owner). |
| default_rsvp_moderation | Moderation | optional | Default moderation for RSVPs from logged-in users (either PENDING or APPROVED). Anonymous RSVPs are always moderated (default to PENDING). |
Request to get RSVP data for an event.
| Field | Type | Label | Description |
|---|---|---|---|
| occasion_id | string | The ID of the event to get RSVP data for. | |
| anonymous_attendee_auth_token | string | optional | If set, and if the token has an RSVP for this even, request that RSVP data in addition to the rest of the RSVP data. (The event creator can always see and moderate anonymous RSVPs.) |
Request to get Events in a formatted per-Occasion structure. i.e. the response will carry duplicate Events with the same ID
if that Event has multiple Occasions in the time frame the client asked for.
These structured Occasions are ordered by start time unless otherwise specified (specifically, EventListingType.NEWLY_ADDED_EVENTS).
Valid GetEventsRequest formats:
{[listing_type: PublicEvents]} (TODO: get ServerPublic/GlobalPublic events you can see){listing_type:MyGroupsEvents|FollowingEvents} (TODO: get events for groups joined or user followed; auth required){post_id:} (get a single event, by its own Post ID or one of its Occasions' Post IDs){listing_type: GroupEvents| GroupEventsPendingModeration, group_id:}
(TODO: get events/events needing moderation for a group)
{author_user_id:, group_id:} (TODO: get events by a user for a group){listing_type: AuthorEvents, author_user_id:} (TODO: get events by a user)| Field | Type | Label | Description |
|---|---|---|---|
| author_user_id | string | optional | Limits results to those by the given author user ID. |
| group_id | string | optional | Limits results to those in the given group ID (via GroupPost association's for the Event's internal Post). |
| time_filter | TimeFilter | optional | Filters returned Occasions by time. |
| attendee_id | string | optional | If set, only returns events that the given user is attending. If attendance_statuses is also set, returns events where that user's status is one of the given statuses. |
| attendance_statuses | AttendanceStatus | repeated | If set, only return events for which the current user's attendance status matches one of the given statuses. If attendee_id is also set, only returns events where the given user's status matches one of the given statuses. |
| post_id | string | optional | Finds Events for the Post with the given ID. The Post should have a PostContext of EVENT or OCCASION. |
| listing_type | EventListingType | The listing type, e.g. ALL_ACCESSIBLE_EVENTS, FOLLOWING_EVENTS, MY_GROUPS_EVENTS, DIRECT_EVENTS, GROUP_EVENTS, GROUP_EVENTS_PENDING_MODERATION. |
|
| search_text | string | optional | Search text for full-text search. |
| occasion_post_ids | string | repeated | Loads multiple events by their occasions' Post IDs - returns one Event per matching Occasion (see GetEventsResponse's own doc), not the requested Occasion's whole parent Event's full occasion list. |
| anonymous_attendee_auth_token | string | optional | Auth token proving ownership of an anonymous RSVP, mirroring GetEventAttendancesRequest.anonymous_attendee_auth_token. Lets an anonymous attendee's own (possibly still-PENDING) EventAttendance and its Occasion.location (when EventInfo.hide_location_until_rsvp_approved is set) surface via each returned Occasion.attendances/current_user_attendance, same as a logged-in user's own RSVP does automatically. |
A list of Events with a maybe-incomplete (see GetEventsRequest) set of their Occasions.
Note that GetEventsResponse may often include duplicate Events with the same ID.
I.E. something like: {events: [{id: a, occasions: [{id: x}]}, {id: a, occasions: [{id: y}]}, ]} is a valid response.
This semantically means: "Event A has both occasions X and Y in the time frame the client asked for."
The client should be able to handle this.
In the React/Tamagui client, this is handled by the Redux store, which
effectively "compacts" all response into its own internal Events store, in a form something like:
{events: {a: {id: a, occasions: [{id: x}, {id: y}]}, ...}, occasionEventIds: {x:a, y:a}}.
(In reality it uses EntityAdapter which is a bit more complicated, but the idea is the same.)
| Field | Type | Label | Description |
|---|---|---|---|
| events | Event | repeated |
The time-based component of an Event. Has a starts_at and ends_at time,
a Location, and an optional Post (and discussion thread) specific to this particular
Occasion in addition to the parent Event.
| Field | Type | Label | Description |
|---|---|---|---|
| event_id | string | ID of the parent Event (i.e. the parent Event.post.id). |
|
| post | Post | Optional Post containing alternate title/link/description for this particular Occasion. Its PostContext should be OCCASION. An Occasion's ID is its post.id - there is no separate surrogate ID. |
|
| info | OccasionInfo | Additional configuration for this Occasion beyond the EventInfo in its parent Event. |
|
| starts_at | google.protobuf.Timestamp | The time the event starts (UTC/Timestamp format). | |
| ends_at | google.protobuf.Timestamp | The time the event ends (UTC/Timestamp format). | |
| location | Location | optional | The location of the event. |
| sync_missing_since | google.protobuf.Timestamp | optional | The time since this event "disappeared" from the sync source. It is up to the owner whether this means it should be deleted. |
| attendances | EventAttendances | optional | RSVP + invite data for this Occasion. |
| current_user_attendance | EventAttendance | optional | If the request was made by a logged-in user, this is the current user's attendance for this Occasion. |
| sync_destinations | SyncDestinationStatus | repeated | SyncDestinations this Occasion has been synced (cross-posted) to, and their status. |
| timezone | string | optional | A time zone for the Occasion. Used when serializing it for, e.g., Facebook or Instagram posts, or generating media. |
To be used for ticketing, RSVPs, etc. Stored as JSON in the database.
| Field | Type | Label | Description |
|---|---|---|---|
| rsvp_info | OccasionRsvpInfo | optional | RSVP configuration and metadata for the Occasion. |
Consolidated type for RSVP info for an Occasion.
Curently, the optional counts below are never returned by the API.
| Field | Type | Label | Description |
|---|---|---|---|
| allows_rsvps | bool | optional | Overrides EventInfo.allows_rsvps, if set, for this Occasion. |
| allows_anonymous_rsvps | bool | optional | Overrides EventInfo.allows_anonymous_rsvps, if set, for this Occasion. |
| max_attendees | uint32 | optional | Overrides EventInfo.max_attendees, if set, for this Occasion. Not yet supported. |
| going_rsvps | uint32 | optional | The number of users who have RSVP'd to the event. |
| going_attendees | uint32 | optional | The number of attendees who have RSVP'd to the event. (RSVPs may have multiple attendees, i.e. guests.) |
| interested_rsvps | uint32 | optional | The number of users who have signaled interest in the event. |
| interested_attendees | uint32 | optional | The number of attendees who have signaled interest in the event. (RSVPs may have multiple attendees, i.e. guests.) |
| invited_rsvps | uint32 | optional | The number of users who have been invited to the event. |
| invited_attendees | uint32 | optional | The number of attendees who have been invited to the event. (RSVPs may have multiple attendees, i.e. guests.) |
Syncs (cross-posts) a single Occasion to one SyncDestination.
| Field | Type | Label | Description |
|---|---|---|---|
| occasion_id | string | The Occasion to sync. | |
| sync_destination_id | string | The SyncDestination to sync it to. |
Time filter that works on the starts_at and ends_at fields of Occasion.
API currently only supports ends_after.
| Field | Type | Label | Description |
|---|---|---|---|
| starts_after | google.protobuf.Timestamp | optional | Filter to events that start after the given time. |
| ends_after | google.protobuf.Timestamp | optional | Filter to events that end after the given time. |
| starts_before | google.protobuf.Timestamp | optional | Filter to events that start before the given time. |
| ends_before | google.protobuf.Timestamp | optional | Filter to events that end before the given time. |
Wire-identical to Author, but with a different name to avoid confusion.
| Field | Type | Label | Description |
|---|---|---|---|
| user_id | string | The user ID of the attendee. | |
| username | string | optional | The username of the attendee. |
| avatar | MediaReference | optional | The attendee's user avatar. |
| real_name | string | optional | |
| permissions | Permission | repeated |
Occasion attendance statuses. State transitions may generally happen in any direction, but:
REQUESTED can only be selected if another user invited the user whose attendance is being described.GOING and NOT_GOING cannot be selected if the Occasion has ended (end time is in the past).WENT and DID_NOT_GO cannot be selected if the Occasion has not started (start time is in the future).
INTERESTED and REQUESTED can apply regardless of whether an event has started or ended.
| Name | Number | Description |
|---|---|---|
| INTERESTED | 0 | The user is (or was) interested in attending. This is the default status. |
| REQUESTED | 1 | Another user has invited the user to the event. |
| GOING | 2 | The user plans to go to the event, or went to the event. |
| NOT_GOING | 3 | The user does not plan to go to the event, or did not go to the event. |
The listing type, e.g. ALL_ACCESSIBLE_EVENTS, FOLLOWING_EVENTS, MY_GROUPS_EVENTS, DIRECT_EVENTS, GROUP_EVENTS, GROUP_EVENTS_PENDING_MODERATION.
Events returned are ordered by start time unless otherwise specified (specifically, NEWLY_ADDED_EVENTS).
| Name | Number | Description |
|---|---|---|
| ALL_ACCESSIBLE_EVENTS | 0 | Gets SERVER_PUBLIC and GLOBAL_PUBLIC events depending on whether the user is logged in, LIMITED events from authors the user is following, and PRIVATE events owned by, or directly addressed to, the current user. |
| FOLLOWING_EVENTS | 1 | Returns events from users the user is following. |
| MY_GROUPS_EVENTS | 2 | Returns events from any group the user is a member of. |
| DIRECT_EVENTS | 3 | Returns DIRECT events that are directly addressed to the user. |
| EVENTS_PENDING_MODERATION | 4 | Returns events pending moderation by the server-level mods/admins. |
| EVENT_TEXT_SEARCH | 5 | Returns posts matching the full-text search_text query, scoped the same way ALL_ACCESSIBLE_POSTS is (plus author_user_id, if provided). Requires search_text parameter. |
| GROUP_EVENTS | 10 | Returns events from a specific group. Requires group_id parameterRequires group_id parameter |
| GROUP_EVENTS_PENDING_MODERATION | 11 | Returns pending_moderation events from a specific group. Requires group_id parameter and user must have group (or server) admin permissions. |
| NEWLY_ADDED_EVENTS | 20 | Returns events from either ALL_ACCESSIBLE_EVENTS or a specific author (with optional author_user_id parameter). Returned Occasions will be ordered by creation time rather than start time. |
Request to get a group or groups by name or ID.
| Field | Type | Label | Description |
|---|---|---|---|
| group_id | string | optional | The ID of the group to get. |
| group_name | string | optional | The name of the group to get. |
| group_shortname | string | optional | The shortname of the group to get. Group shortname search is case-insensitive. |
| listing_type | GroupListingType | The group listing type. | |
| page | int32 | optional | The page of results to get. |
Response to a GetGroupsRequest.
| Field | Type | Label | Description |
|---|---|---|---|
| groups | Group | repeated | The groups that matched the request. |
| has_next_page | bool | Whether there are more groups to get. |
Request to get members of a group.
| Field | Type | Label | Description |
|---|---|---|---|
| group_id | string | The ID of the group to get members of. | |
| username | string | optional | The username of the members to search for. |
| group_moderation | Moderation | optional | The membership status to filter members by. If not specified, all members are returned. |
| page | int32 | optional | The page of results to get. |
Response to a GetMembersRequest.
| Field | Type | Label | Description |
|---|---|---|---|
| members | Member | repeated | The members that matched the request. |
| has_next_page | bool | Whether there are more members to get. |
Groups are a way to organize users and posts (and thus events). They can be used for many purposes,
| Field | Type | Label | Description |
|---|---|---|---|
| id | string | The group's unique ID. | |
| name | string | Mutable name of the group. Must be unique, such that the derived shortname is also unique. |
|
| shortname | string | Immutable shortname of the group. Derived from changes to name when the Group is updated. |
|
| description | string | A description of the group. | |
| avatar | MediaReference | optional | An avatar for the group. |
| default_membership_permissions | Permission | repeated | The default permissions for new members of the group. |
| default_membership_moderation | Moderation | The default moderation for new members of the group. Valid values are PENDING (requires a moderator to let you join) and UNMODERATED. | |
| default_post_moderation | Moderation | The default moderation for new posts in the group. | |
| default_event_moderation | Moderation | The default moderation for new events in the group. | |
| visibility | Visibility | LIMITED visibility groups are only visible to members. PRIVATE groups are only visibile to users with the ADMIN group permission. | |
| member_count | uint32 | The number of members in the group. | |
| post_count | uint32 | The number of posts in the group. | |
| event_count | uint32 | The number of events in the group. | |
| non_member_permissions | Permission | repeated | The permissions given to non-members of the group. |
| current_user_membership | Membership | optional | The membership for the current user, if any. |
| created_at | google.protobuf.Timestamp | The time the group was created. | |
| updated_at | google.protobuf.Timestamp | optional | The time the group was last updated. |
Used when fetching group members using the GetMembers RPC.
| Field | Type | Label | Description |
|---|---|---|---|
| user | User | The user. | |
| membership | Membership | The user's membership (or join request, or invitation, or both) in the group. |
The type of group listing to get.
| Name | Number | Description |
|---|---|---|
| ALL_GROUPS | 0 | Get all groups (visible to the current user). |
| MY_GROUPS | 1 | Get groups the current user is a member of. |
| REQUESTED_GROUPS | 2 | Get groups the current user has requested to join. |
| INVITED_GROUPS | 3 | Get groups the current user has been invited to. |
Bird (https://bird.com, formerly MessageBird) Config -- an alternative SMS verification
provider to TwilioConfig, with a simpler single-API-key auth model. See
docs/contact_integrations.md for full setup steps.
| Field | Type | Label | Description |
|---|---|---|---|
| bird_enabled | bool | ||
| bird_access_key | string | The Bird workspace's API access key. Never serialized once written. | |
| bird_from | string | The originator for outbound verification SMS -- an owned number, alphanumeric sender ID (3-11 chars), or short code, as configured in the Bird workspace. Not secret. | |
| bird_region | string | Which Bird API region to call ("us1" or "eu1", per Bird's own regional API hosts). Not secret. Empty defaults to "us1". | |
| bird_webhook_signing_key | string | optional | Optional -- the Standard Webhooks signing secret (starts with whsec_) for the SMS channel subscription delivering to /contact_integrations/bird/receive, used to verify the webhook-id/webhook-timestamp/webhook-signature headers on inbound deliveries (HMAC-SHA256; see https://www.standardwebhooks.com and docs/contact_integrations.md). Blank/unset means inbound deliveries are accepted without signature verification. optional (not plain string) for the same "Some/None distinguishable from a client without ever seeing the real value" reason as TwilioConfig.twilio_webhook_signing_key. |
The conductor's live view of currently-held locks - one ClusterResourceLock per distinct
holder (a given namespace can appear at most once here - LockClusterResources never grants a
ClusterResource it's already granted that same namespace_id, and folds any additional
resources into that namespace's existing entry rather than creating a second one - see that
RPC's own doc). With a limits entry above 1 (see ClusterResourceLimit's own doc), more than
one distinct namespace can hold the same ClusterResource at once, so there can be more
entries here than there are ClusterResource values.
See ClusterResources.conductor_state.
| Field | Type | Label | Description |
|---|---|---|---|
| locks | ClusterResourceLock | repeated | Locks currently held by the conductor. Mutations are only made by FreeClusterResource and LockClusterResource, not ConfigureServer. Changes to locks do not produce a new ServerConfiguration version. |
| limits | ClusterResourceLimit | repeated | How many distinct namespaces may concurrently hold each ClusterResource's lock - e.g. only one headless browser at a time, but a handful of ffmpeg/ImageMagick conversions in parallel across the cluster, since those are far lighter-weight. Any ClusterResource not present here - including on a cluster that's never had ConfigureServer touch limits at all - defaults to 1 (see ClusterTab.elm's matching client-side default, shown/edited there as "Browser Instance Limit"/"FFMPEG Process Limit"/"ImageMagick Process Limit"). These are changed by ConfigureServer (gated on EDIT_CLUSTER_SETTINGS, like the rest of cluster_resources) and create a new ServerConfiguration version, unlike locks above. |
One ClusterResource's configured concurrency limit - a single resource/limit pairing per
message (both fields are singleton lists in practice; see ClusterConductorState.limits's own
doc for why a ClusterResource missing from every ClusterResourceLimit here defaults to 1
rather than 0).
| Field | Type | Label | Description |
|---|---|---|---|
| resource | ClusterResource | repeated | |
| limit | uint32 | repeated | The number of distinct namespaces that may concurrently hold a lock on resource. |
One namespace's currently-held lock on one or more ClusterResources, and when it acquired
them - shown in ClusterTab's Elm UI so an admin can tell a genuinely stuck lock (acquired
long ago, its holder's job surely long dead) from one just in normal, brief use, and reach for
free_all_cluster_resources (a bin/ admin tool - see its own doc) accordingly.
| Field | Type | Label | Description |
|---|---|---|---|
| lock_holder_namespace_id | string | The namespace_id (see ClusterResources.namespace_id) holding this lock. |
|
| resources | ClusterResource | repeated | Which resources this lock covers. |
| acquired_at | google.protobuf.Timestamp | When LockClusterResources granted this lock. |
Coordinates a small piece of shared, cluster-wide state across multiple independent Rellm
server instances that are otherwise fully isolated from each other (separate databases, separate
FederationInfo, etc.) but happen to run on shared underlying
infrastructure (e.g. several Kubernetes namespaces sharing one small node pool). Currently used
for exactly one thing: making sure only one instance has a headless Chrome/Brave browser open at
any given moment (for generating link preview images), since launching several at once can
exhaust a shared node's CPU/memory. One participating instance is designated the "conductor" (see
conductor_host) and brokers locks via
LockClusterResources/
FreeClusterResources; every instance in the cluster --
including the conductor itself - sets its own ClusterResources pointing at whichever host
that is.
See ServerConfiguration.cluster_resources's own doc for who can see/edit this.
| Field | Type | Label | Description |
|---|---|---|---|
| namespace_id | string | Identifies this instance to the conductor - e.g. its Kubernetes namespace. Passed as LockClusterResourcesRequest.namespace_id/FreeClusterResourcesRequest.namespace_id so the conductor knows who's asking, and echoed back as ClusterResourceLock.lock_holder_namespace_id while this instance holds a lock. By convention (not enforced - see cluster_shared_secret's own doc), the conductor sets its own namespace_id equal to its own conductor_host; clients (e.g. the Elm ClusterTab) use that convention purely for display, to tell "this instance is the conductor" from "some other instance is." |
|
| conductor_host | string | DNS hostname of whichever instance in the cluster is the "conductor" - the single instance that actually brokers LockClusterResources/ FreeClusterResources calls for every other instance (including, by convention, itself - see conductor_state). Every instance in the cluster points this at the same host. |
Note: callers should resolve this the same way any other cross-server Rellm call does -- via GET {conductor_host}/backend_host first, falling back to conductor_host itself - rather than connecting to it directly, in case the conductor sits behind an ExternalCDNConfig. |
| cluster_shared_secret | string | | Shared secret proving a LockClusterResources/FreeClusterResources caller is a legitimate member of this cluster, passed as the cluster-shared-secret gRPC metadata header (not a request field - there's no per-user auth involved in these calls at all, just this secret). The receiving server checks it against its own stored cluster_shared_secret - that's the entire authorization check: knowing the secret is what makes a caller entitled to treat that server as the conductor, regardless of what that server's own namespace_id/conductor_host happen to say (see namespace_id's own doc on that being a display-only convention). Write-only, like FacebookAuthConfig.app_secret/ WebPushConfig.private_vapid_key - GetServerConfiguration never sends the real value back to any client (not even an admin), and an empty incoming value on ConfigureServer means "leave the stored secret alone," not "clear it." Should never be transmitted over a non-TLS connection. |
| conductor_state | ClusterConductorState | optional | The conductor's live view of who currently holds each ClusterResource's lock. Only ever populated on whichever instance actually receives (and grants) LockClusterResources calls -- in a correctly configured cluster, that's the one instance every participant points conductor_host at (see that field's own doc), but nothing server-side enforces that; every other instance simply never gets asked to hold this state. Reflects the database directly, updated in place by LockClusterResources/FreeClusterResources - unlike the rest of ServerConfiguration, ConfigureServer never lets a caller change this, and it isn't versioned the way other ConfigureServer changes are. |
Overrides the app's default / page (the combined Events+Posts feed). Unlike a regular
CustomNavigationTab, this has no path (it's always /) and no icon/title (the server's
own name/logo are always shown for the Home tab in the nav, regardless of what it links to).
| Field | Type | Label | Description |
|---|---|---|---|
| tab | NavigationTab | What / renders. Only HOME_TAB (the default, combined Events+Posts feed), EVENTS_TAB, or POSTS_TAB are valid here - never PEOPLE_TAB/ABOUT_TAB. |
|
| post_id | string | Renders a specific Post at / instead (e.g. for a custom business site's landing page). |
|
| pinned_post_ids | string | repeated | Posts pinned to the top of the home page, above its normal content. Loaded the same way StarredPanel loads its own starred posts (i.e., conditionally fetching each pinned post's backing Event alongside it, for posts that are actually about an Event). |
| show_events_strip | bool | Shows the Events strip (the same horizontal upcoming-events row the default HOME_TAB always shows above its Posts feed) above target's own content. Only meaningful when target is post_id (pins an Events strip above that single Post); has no effect when target is unset/HOME_TAB (the strip is already shown) or POSTS_TAB (equivalent to just leaving target unset). |
|
| default_events_strip_to_row | bool | Whenever an Events strip is shown above other content - show_events_strip is set, or target is unset/HOME_TAB (whose strip is always shown) - whether it defaults to its row/list layout instead of a calendar. Unset defaults to the calendar layout. |
|
| default_events_strip_calendar_display_mode | CalendarDisplayMode | Whenever an Events strip is shown above other content (see default_events_strip_to_row's own doc) and defaults to the calendar layout (default_events_strip_to_row is unset), which granularity it opens to. Defaults to CALENDAR_DISPLAY_WEEK. |
Either one of the app's predefined tabs, a Post, or a user profile - reachable at path.
| Field | Type | Label | Description |
|---|---|---|---|
| tab | NavigationTab | Links to one of the app's predefined tabs/pages. | |
| post_id | string | Links to a specific Post (e.g. for a custom business site's page). | |
| is_profile | bool | Indicates the custom tab is for an actual user profile - path is that user's username. Ultimately this isn't very "custom" in terms of the URL scheme, just it being a navigation tab. |
|
| emoji_icon | string | Emoji shown as the tab's icon (e.g. "🎪"). | |
| icon_media_id | string | Media ID (see Media APIs) of an image shown as the tab's icon. |
|
| title | string | optional | Title shown for the tab. Defaults to the predefined tab's/Post's title if unset. |
| path | string | The path this tab is reachable at, e.g. gigs for a band's /gigs link to the Events page, or weddings for a Post about wedding offerings. Must be distinct across every entry in CustomNavigationTabSet.tabs. Note: events, posts, people, and about are reserved -- each may only be used to (redundantly) point back at its own matching predefined tab, never remapped to a different tab or a Post. / itself is never reachable this way - it's overridden via CustomNavigationTabSet.home instead. |
If set, overrides the default tab set for the Elm navigation on a Rellm instance.
| Field | Type | Label | Description |
|---|---|---|---|
| home | CustomHomePage | optional | Overrides the default / page. If unset, the default combined Events+Posts feed is used. |
| tabs | CustomNavigationTab | repeated | Overrides the default tab set (EVENTS_TAB, POSTS_TAB, PEOPLE_TAB, ABOUT_TAB) entirely. Note: existing /events, /posts, /people, and /about paths are reserved for their matching predefined tab - see CustomNavigationTab.path's own doc. / itself is overridden via home above instead. |
| tab_style | NavigationTabStyle | How every tab (Home excluded - it always shows the server's own logo/name) is laid out in the Elm nav. Purely cosmetic: it changes nothing about which tabs exist, their order, or where they link - see NavigationTabStyle below. Defaults to NAVIGATION_TAB_ICON_ONLY (proto enum value 0) both when CustomNavigationTabSet itself is unset and for any config saved before this field existed. |
Specific settings for Events.
| Field | Type | Label | Description |
|---|---|---|---|
| visible | bool | Hide the Events tab from the user with this flag. | |
| default_moderation | Moderation | Only UNMODERATED and PENDING are valid. When UNMODERATED, user reports may transition status to PENDING. When PENDING, users' SERVER_PUBLIC or GLOBAL_PUBLIC posts will not be visible until a moderator approves them. LIMITED visiblity posts are always visible to targeted users (who have not blocked the author) regardless of default_moderation. |
|
| default_visibility | Visibility | Only SERVER_PUBLIC and GLOBAL_PUBLIC are valid. GLOBAL_PUBLIC is only valid if default_user_permissions contains `GLOBALLY_PUBLISH_[USERS |
|
| alias_singular | string | optional | Can be used to rename, e.g., "Event" to "Gig" or "Performance" |
| alias_plural | string | optional | Can be used to rename, e.g. "Events" to "Show," "Game," "Competition" |
| enable_replies | bool | optional | Works the same as for Posts. |
| calendar_lookback_days | uint32 | optional | How far to look back for the "Upcoming Events" tab in the server's UI. Defaults to 14. Servers with fewer events may want to set to a higher value. |
| default_calendar_display_mode | CalendarDisplayMode | What the Events Calendar's default UI mode will be. Defaults to CALENDAR_DISPLAY_WEEK. Servers with fewer events may want to set CALENDAR_DISPLAY_MONTH, or with more to CALENDAR_DISPLAY_DAY. |
|
| show_started_or_long_events_by_default | bool | Affects the Elm UI "â–½" button on EventsPages (embedded or no). When this is false, that filter defaults to "on." When true, that filter defaults to "off." |
For a band site (where you want to show your "true calendar"), this is best set to true. For a site where you have lots of event postings, it's best set to false. |
Useful for setting your Rellm instance up to run underneath a CDN.
By default, the web client uses window.location.hostname to determine the backend server.
If set, the web client will use this value instead. NOTE: Only applies to Tamagui web client for now.
| Field | Type | Label | Description |
|---|---|---|---|
| frontend_host | string | The domain where the frontend is hosted. For example, jonline.io. Typically your CDN (like Cloudflare) should own the DNS for this domain. | |
| backend_host | string | The domain where the backend is hosted. For example, jonline.io.itsj.online. Typically your Kubernetes provider should own DNS for this domain. | |
| secure_media | bool | (TODO) When set, the HTTP GET /media/<id>?<authorization> endpoint will be disabled by default on the HTTP (non-secure) server that sends data to the CDN. Only requests from IPs in media_ipv4_allowlist and media_ipv6_allowlist will be allowed. |
|
| media_ipv4_allowlist | string | optional | Whitespace- and/or comma- separated list of IPv4 addresses/ranges to whom media data may be served. Only applicable if secure_media is true. For reference, Cloudflare's are at https://www.cloudflare.com/ips-v4. |
| media_ipv6_allowlist | string | optional | Whitespace- and/or comma- separated list of IPv6 addresses/ranges to whom media data may be served. Only applicable if secure_media is true. For reference, Cloudflare's are at https://www.cloudflare.com/ips-v6. |
| cdn_grpc | bool | (TODO) When implemented, this actually changes the whole Rellm protocol (in terms of ports). When enabled, Rellm should not server a secure site on HTTPS, and instead serve the Tonic gRPC server there (on port 443). Jonine clients will need to be updated to always seek out a secure client on port 443 when this feature is enabled. This would let Rellm leverage Cloudflare's DDOS protection and performance on gRPC as well as HTTP. (This is a Cloudflare-specific feature requirement.) |
Settings for a feature (e.g. People, Groups, Posts, Events, Media). Encompasses both the feature's visibility and moderation settings.
| Field | Type | Label | Description |
|---|---|---|---|
| visible | bool | Hide the Posts or Events tab from the user with this flag. | |
| default_moderation | Moderation | Only UNMODERATED and PENDING are valid. When UNMODERATED, user reports may transition status to PENDING. When PENDING, users' SERVER_PUBLIC or GLOBAL_PUBLIC posts will not be visible until a moderator approves them. LIMITED visiblity posts are always visible to targeted users (who have not blocked the author) regardless of default_moderation. |
|
| default_visibility | Visibility | Only SERVER_PUBLIC and GLOBAL_PUBLIC are valid. GLOBAL_PUBLIC is only valid if default_user_permissions contains `GLOBALLY_PUBLISH_[USERS |
|
| alias_singular | string | optional | Can be used to rename, e.g., "Person" to "Contributor" or "Group" to "Community" |
| alias_plural | string | optional | Can be used to rename, e.g. "Groups" to "Subtwaddits" or "People" to "Folks" |
Releases resources this namespace_id previously locked via
LockClusterResources. A no-op (not an error) for any
resource namespace_id doesn't currently hold - e.g. safe to call unconditionally during
cleanup even if the matching lock attempt itself failed or was never confirmed.
| Field | Type | Label | Description |
|---|---|---|---|
| namespace_id | string | This instance's own ClusterResources.namespace_id - must match whichever namespace_id is recorded as the current holder for a resource to actually be released. |
|
| resources | ClusterResource | repeated | Which resources to release. |
See LockClusterResources.
| Field | Type | Label | Description |
|---|---|---|---|
| namespace_id | string | This instance's own ClusterResources.namespace_id. |
|
| resources | ClusterResource | repeated | Which resources to lock - see the ClusterResource enum for what exists. |
See LockClusterResources.
| Field | Type | Label | Description |
|---|---|---|---|
| granted | bool | Whether every requested resource was successfully locked for namespace_id. false means none were locked (never a partial grant) - at least one of them is already held, by namespaces other than this one, by as many distinct holders as its configured ClusterResourceLimit allows (see that message's own doc); see holder. There's no server-side wait/queueing: a caller that gets false should back off and call LockClusterResources again later. |
|
| holder | string | optional | Set only when granted is false: one of the namespaces already holding a requested (and therefore denied) resource. |
Whether this server's /market is open -- an explicit, admin-set toggle independent of
StripeConfig.stripe_enabled (an admin can configure Stripe credentials without opening the
storefront yet, or temporarily close it without touching those credentials). See
ServerConfiguration.market_settings's own doc on why this lives outside StripeConfig: it's
the one bit that has to stay visible to non-admins for federated multi-server Market browsing to
work at all.
| Field | Type | Label | Description |
|---|---|---|---|
| enabled | bool | ||
| stripe_configured | bool | Whether Stripe is actually usable right now -- stripe_config.stripe_enabled is true AND a stripe_secret_key is on file. Computed live on every GetServerConfiguration (never read back from whatever was last saved to market_settings itself), and -- like enabled above -- deliberately never stripped for non-admins: it's the public "can I actually buy something here" signal a buyer needs (e.g. to grey out /market/product/:id's "Buy" button with a "Stripe is not configured" message) without ever exposing StripeConfig itself, which stays admin-only. |
Media is a special type and less customizable than "Features."
| Field | Type | Label | Description |
|---|---|---|---|
| visible | bool | Hide the Posts or Events tab from the user with this flag. | |
| default_moderation | Moderation | Only UNMODERATED and PENDING are valid. When UNMODERATED, user reports may transition status to PENDING. When PENDING, users' SERVER_PUBLIC or GLOBAL_PUBLIC posts will not be visible until a moderator approves them. LIMITED visiblity posts are always visible to targeted users (who have not blocked the author) regardless of default_moderation. |
|
| default_visibility | Visibility | Only SERVER_PUBLIC and GLOBAL_PUBLIC are valid. GLOBAL_PUBLIC is only valid if default_user_permissions contains `GLOBALLY_PUBLISH_[USERS |
|
| default_media_allocation_bytes | uint64 | Default media storage allocation for newly created users. Defaults to 10MB. |
Specific settings for Posts.
| Field | Type | Label | Description |
|---|---|---|---|
| visible | bool | Hide the Posts tab from the user with this flag. | |
| default_moderation | Moderation | Only UNMODERATED and PENDING are valid. When UNMODERATED, user reports may transition status to PENDING. When PENDING, users' SERVER_PUBLIC or GLOBAL_PUBLIC posts will not be visible until a moderator approves them. LIMITED visiblity posts are always visible to targeted users (who have not blocked the author) regardless of default_moderation. |
|
| default_visibility | Visibility | Only SERVER_PUBLIC and GLOBAL_PUBLIC are valid. GLOBAL_PUBLIC is only valid if default_user_permissions contains `GLOBALLY_PUBLISH_[USERS |
|
| alias_singular | string | optional | Can be used to rename, e.g., "Post" "Highlight" or "Squirt" |
| alias_plural | string | optional | Can be used to rename, e.g. "Posts" to "Splurts" or "Memories" |
| enable_replies | bool | optional | Controls whether replies are shown in the UI. Note that users' ability to reply is controlled by the REPLY_TO_POSTS permission. |
Color in ARGB hex format (i.e 0xAARRGGBB).
| Field | Type | Label | Description |
|---|---|---|---|
| primary | uint32 | optional | App Bar/primary accent color. |
| navigation | uint32 | optional | Nav/secondary accent color. |
| author | uint32 | optional | Color used on author of a post in discussion threads for it. |
| admin | uint32 | optional | Color used on author for admin posts. |
| moderator | uint32 | optional | Color used on author for moderator posts. |
Configuration for a Rellm server instance.
| Field | Type | Label | Description |
|---|---|---|---|
| server_info | ServerInfo | optional | The name, description, logo, color scheme, etc. of the server. |
| federation_info | FederationInfo | optional | The federation configuration for the server. |
| anonymous_user_permissions | Permission | repeated | Permissions for a user who isn't logged in to the server. Allows admins to disable certain features for anonymous users. Valid values are VIEW_USERS, VIEW_GROUPS, VIEW_POSTS, and VIEW_EVENTS. |
| default_user_permissions | Permission | repeated | Default user permissions given to a new user. Users with MODERATE_USERS permission can also grant/revoke these permissions for others. Valid values are VIEW_USERS, PUBLISH_USERS_LOCALLY, PUBLISH_USERS_GLOBALLY, VIEW_GROUPS, CREATE_GROUPS, PUBLISH_GROUPS_LOCALLY, PUBLISH_GROUPS_GLOBALLY, JOIN_GROUPS, VIEW_POSTS, CREATE_POSTS, PUBLISH_POSTS_LOCALLY, PUBLISH_POSTS_GLOBALLY, VIEW_EVENTS, CREATE_EVENTS, PUBLISH_EVENTS_LOCALLY, and PUBLISH_EVENTS_GLOBALLY. |
| basic_user_permissions | Permission | repeated | Permissions grantable by a user with the GRANT_BASIC_PERMISSIONS permission. Valid values are VIEW_USERS, PUBLISH_USERS_LOCALLY, PUBLISH_USERS_GLOBALLY, VIEW_GROUPS, CREATE_GROUPS, PUBLISH_GROUPS_LOCALLY, PUBLISH_GROUPS_GLOBALLY, JOIN_GROUPS, VIEW_POSTS, CREATE_POSTS, PUBLISH_POSTS_LOCALLY, PUBLISH_POSTS_GLOBALLY, VIEW_EVENTS, CREATE_EVENTS, PUBLISH_EVENTS_LOCALLY, and PUBLISH_EVENTS_GLOBALLY. |
| custom_tabs | CustomNavigationTabSet | optional | |
| people_settings | FeatureSettings | Configuration for users on the server. If default visibility is GLOBAL_PUBLIC, default_user_permissions must contain PUBLISH_USERS_GLOBALLY. |
|
| group_settings | FeatureSettings | Configuration for groups on the server. If default visibility is GLOBAL_PUBLIC, default_user_permissions must contain PUBLISH_GROUPS_GLOBALLY. |
|
| post_settings | PostSettings | Configuration for posts on the server. If default visibility is GLOBAL_PUBLIC, default_user_permissions must contain PUBLISH_POSTS_GLOBALLY. |
|
| event_settings | EventSettings | Configuration for events on the server. If default visibility is GLOBAL_PUBLIC, default_user_permissions must contain PUBLISH_EVENTS_GLOBALLY. |
|
| media_settings | MediaSettings | Configuration for media on the server. If default visibility is GLOBAL_PUBLIC, default_user_permissions must contain PUBLISH_MEDIA_GLOBALLY. |
|
| market_settings | MarketSettings | Public, non-secret "is this server's Market open" signal -- unlike stripe_config (which holds real credentials and is Admin-only, see that field's own doc), this is never stripped for a non-admin/unauthenticated caller. Lets a client decide whether to show this server's Market section at all (e.g. when browsing a federated list of servers) without needing to be an admin here just to check -- see rellm.proto's own "Federated Markets" doc section. |
|
| external_cdn_config | ExternalCDNConfig | optional | If set, enables External CDN support for the server. This means that the non-secure HTTP server (on port 80) will not redirect to the secure server, and instead serve up Tamagui Web/Flutter clients directly. This allows you to point Cloudflare's "CNAME HTTPS Proxy" feature at your Rellm server to serve up HTML/CS/JS and Media files with caching from Cloudflare's CDN. See ExternalCDNConfig for more details on securing this setup. |
| cluster_resources | ClusterResources | optional | Cluster-internal coordination state - see ClusterResources's own doc. Visible to any logged-in admin (unlike most fields here, this describes infrastructure topology rather than anything end users need, so it's stripped entirely from GetServerConfiguration for non-admins/anonymous callers); editing it via ConfigureServer additionally requires the EDIT_CLUSTER_SETTINGS permission. |
| private_user_strategy | PrivateUserStrategy | Strategy when a user sets their visibility to PRIVATE. Defaults to ACCOUNT_IS_FROZEN. |
|
| authentication_features | AuthenticationFeature | repeated | (TODO) Allows admins to enable/disable creating accounts and logging in. Eventually, external auth too hopefully! |
| web_push_config | WebPushConfig | optional | Web Push (VAPID) configuration for the server. |
| supported_contact_protocols | ContactProtocol | repeated | Which ContactProtocols (tel:/mailto:) this server currently accepts -- drives ContactMethod.supported_by_server (users.proto, which can't reference this message directly -- see that field's own doc for why). Settable via ConfigureServer, which errors rather than silently dropping an invalid entry: CONTACT_PROTOCOL_TEL requires an enabled TwilioConfig/BirdConfig/ TelnyxConfig in that same request, and CONTACT_PROTOCOL_MAILTO is always rejected (no email provider exists yet). Edited via the "Enable SMS Sending"/"Enable Email Sending" toggles on the Contact Integrations tab's "SMS Configuration"/"Email Configuration" sections (ContactIntegrationsTab.smsConfigurationSection/ emailConfigurationSection) -- see docs/contact_integrations.md. |
| preferred_verification_apis | ContactVerificationAPI | repeated | A server-preferred order of ContactVerificationAPI providers to try first when more than one of twilio_config/bird_config/telnyx_config below is enabled -- see contact_verification::available_verification_apis for the full preference-then-fallback ordering this feeds into ServerConfiguration.available_verification_apis below. Even when this is blank, an enabled provider is still tried (in the fixed default order Twilio/Bird/Telnyx) -- this field only matters when more than one is enabled and the admin wants a specific one tried first. Only serialized for admin users. |
| available_verification_apis | ContactVerificationAPI | repeated | Derived from whether TwilioConfig.twilio_enabled/ BirdConfig.bird_enabled/ TelnyxConfig.telnyx_enabled is true, ordered per preferred_verification_apis above. Serialized to every caller (not admin-only, unlike preferred_verification_apis/twilio_config/bird_config/telnyx_config) -- this is what a non-admin client should check to decide whether to show verification UI at all, without exposing any provider configuration. Independent of supported_contact_protocols above -- that's the admin's own on/off toggle (can disable tel: contact even while a provider stays enabled/configured), this is purely "is at least one provider actually configured." |
| twilio_config | TwilioConfig | optional | Twilio Config -- see TwilioConfig. Only serialized for admin users. |
| bird_config | BirdConfig | optional | Bird (bird.com, formerly MessageBird) Config -- a cheaper alternative to TwilioConfig for SMS verification; see BirdConfig's own doc. Only serialized for admin users. |
| stripe_config | StripeConfig | optional | Stripe Config, backing the Marketplace (market.proto). Only serialized for admin users. |
| telnyx_config | TelnyxConfig | optional | Telnyx Config -- another alternative SMS verification provider to TwilioConfig (see TelnyxConfig's own doc). Only serialized for admin users. |
| stalwart_config | StalwartConfig | optional | Unlike the other configs, at least at the moment, the integration with Stalwart is designed to be in-cluster, not over the web. It relies on unsecured /email endpoint on port 27705 to receive mail from a Stalwart deployed within the same Kubernetes cluster. |
This could be extended in the future to allow sending mail with Stalwart, but that's TBD. |
User-facing information about the server displayed on the "about" page.
| Field | Type | Label | Description |
|---|---|---|---|
| name | string | optional | Name of the server. |
| short_name | string | optional | Short name of the server. Used in URLs, etc. (Currently unused.) |
| description | string | optional | Description of the server. |
| privacy_policy | string | optional | The server's privacy policy. Will be displayed during account creation and on the /about page. |
| logo | ServerLogo | optional | Multi-size logo data for the server. |
| web_user_interface | WebUserInterface | optional | The web UI to use (React/Tamagui (default) vs. Flutter Web) |
| colors | ServerColors | optional | The color scheme for the server. |
| media_policy | string | optional | The media policy for the server. Will be displayed during account creation and on the /about page. |
| recommended_server_hosts | string | repeated | Deprecated. This will be replaced with FederationInfo soon. |
Logo data for the server. Built atop Rellm Media APIs.
| Field | Type | Label | Description |
|---|---|---|---|
| squareMediaId | string | optional | The media ID for the square logo. |
| squareMediaIdDark | string | optional | The media ID for the square logo in dark mode. |
| wideMediaId | string | optional | The media ID for the wide logo. |
| wideMediaIdDark | string | optional | The media ID for the wide logo in dark mode. |
Stalwart is an extablished, open-source Rust email/contact/calendar server (think an Outlook or Google Workspace competitor). Currently Rellm supports receiving emails via Stalwart webhook configurations.
| Field | Type | Label | Description |
|---|---|---|---|
| stalwart_receiving_enabled | bool | Enables receiving emails from the private, unsecured cluster-facing HTTP server at :27705/email. |
Stripe credentials backing the Marketplace (market.proto). Used both to create Checkout
Sessions/off-session renewal PaymentIntents (stripe_secret_key) and to verify incoming
webhook deliveries (stripe_webhook_signing_secret).
| Field | Type | Label | Description |
|---|---|---|---|
| stripe_enabled | bool | ||
| stripe_secret_key | string | Stripe Secret Key (starts with sk_), used as Bearer auth for all Stripe API calls made by this server (Checkout Session creation, off-session renewal charges). Never serialized once written -- same write-only treatment as TwilioConfig.twilio_api_key_secret. |
|
| stripe_publishable_key | string | Stripe Publishable Key (starts with pk_). Not secret -- kept here (rather than derived from stripe_secret_key) so a future client-side Stripe Elements integration has what it needs, even though the current Checkout-based flow doesn't use it server-side at all. |
|
| stripe_webhook_signing_secret | string | Signing secret (starts with whsec_) for the /webhooks/stripe endpoint, used to verify the Stripe-Signature header on incoming webhook deliveries. Never serialized once written -- same write-only treatment as stripe_secret_key above. |
Telnyx (https://telnyx.com) Config -- an alternative SMS verification provider to
TwilioConfig, with a simpler single-API-key auth model like
BirdConfig's. See docs/contact_integrations.md for full setup steps.
| Field | Type | Label | Description |
|---|---|---|---|
| telnyx_enabled | bool | ||
| telnyx_api_key | string | The Telnyx v2 API Key (starts with KEY), used as Bearer auth for Telnyx's Messaging API (POST /v2/messages). Never serialized once written -- same write-only treatment as TwilioConfig.twilio_api_key_secret/ BirdConfig.bird_access_key. |
|
| telnyx_from_number | string | The Telnyx-provisioned sending number for outbound verification SMS (E.164, e.g. a toll-free number). Not secret. | |
| telnyx_messaging_profile_id | string | The Telnyx Messaging Profile ID that telnyx_from_number is assigned to -- required by Telnyx's Messaging API to actually send (messaging_profile_id on POST /v2/messages). Not secret. |
|
| telnyx_webhook_signing_key | string | optional | Optional -- Telnyx's account-level public key (Mission Control Portal -> Keys & Credentials -> Public Key), used to verify the telnyx-signature-ed25519/telnyx-timestamp headers on inbound deliveries to /contact_integrations/telnyx/receive (Ed25519; see https://developers.telnyx.com/docs/messaging/messages/receiving-webhooks and docs/contact_integrations.md). Actually a public key, not a secret, but kept write-only (never serialized once written) for the same "don't echo config back" treatment as every other credential here. Unset means inbound deliveries are accepted without signature verification. optional (not plain string) for the same "Some/None distinguishable from a client without ever seeing the real value" reason as TwilioConfig.twilio_webhook_signing_key. |
Twilio credentials, authenticated via a Twilio API Key (twilio_api_key_sid/
twilio_api_key_secret) -- deliberately not the account's own Auth Token. A server's Auth
Token is a single unscoped, unrevocable-without-rotating-everything credential with full access
to the whole Twilio account; an API Key is its own separate, individually-revocable credential
pair meant for exactly this kind of integration. twilio_account_sid is still required (Twilio
resource URLs are always addressed by the actual Account SID), but it is not used to
authenticate -- only the API Key SID/Secret pair is. See
https://www.twilio.com/docs/iam/api-keys/restricted-api-keys for the recommended
permission when creating one: /twilio/messaging/messages/create (nothing else is needed just
to send verification SMS). See docs/contact_integrations.md for full setup steps (API key
creation, inbound webhook registration), and ContactMethod
(users.proto) for how a verified tel: contact method surfaces this provider.
| Field | Type | Label | Description |
|---|---|---|---|
| twilio_enabled | bool | ||
| twilio_account_sid | string | The Twilio Account SID (starts with AC). Used only in the API's URL path -- never as an authentication credential. Public (among admins) -- freely serialized. |
|
| twilio_api_key_sid | string | The Twilio API Key's SID (starts with SK), used as the Basic Auth username. Public (among admins) -- freely serialized; it's useless without the Secret below, same as a username alone. |
|
| twilio_api_key_secret | string | The Twilio API Key's Secret, used as the Basic Auth password. Never serialized once written. | |
| twilio_from_number | string | The Twilio-provisioned sending number for outbound verification SMS. Not secret. | |
| twilio_webhook_signing_key | string | optional | Optional -- the Twilio Account's Auth Token, used only to verify the X-Twilio-Signature header on inbound deliveries to /contact_integrations/twilio/receive (see https://www.twilio.com/docs/usage/webhooks/webhooks-security and docs/contact_integrations.md). Never used to authenticate outbound API calls -- this message's own doc explains why the Auth Token is deliberately excluded from that role; this is the one narrow exception, since signature verification is the one thing only the Auth Token (not an API Key) can do. Unset means inbound deliveries are accepted without signature verification. Write-only, like every other credential here, but distinctly from those: optional so a client can tell whether a key is configured (Some/None) without ever seeing its real value -- once set, to_proto blanks this to Some("") (not None), so "configured but hidden" and "never configured" stay distinguishable. Sending an empty value back on ConfigureServer means "leave whatever's already stored alone," same as every other write-only field's blank-means-no-op rule. |
Web Push (VAPID) configuration for the server.
| Field | Type | Label | Description |
|---|---|---|---|
| public_vapid_key | string | Public VAPID key for the server. | |
| private_vapid_key | string | Private VAPID key for the server. Never serialized to the client. Admins: Edit this in the database's JSONB column directly. |
Authentication features that can be enabled/disabled by the server admin.
| Name | Number | Description |
|---|---|---|
| AUTHENTICATION_FEATURE_UNKNOWN | 0 | An authentication feature that is not known to the server. (Likely, the client and server use different versions of the Rellm protocol.) |
| CREATE_ACCOUNT | 1 | Users can sign up for an account. |
| LOGIN | 2 | Users can sign in with an existing account. |
The Events Calendar's default UI granularity.
| Name | Number | Description |
|---|---|---|
| CALENDAR_DISPLAY_WEEK | 0 | Shows a 7-day week at a time. Good default for most servers. |
| CALENDAR_DISPLAY_MONTH | 1 | Shows a full month at a time. Better for servers with fewer events. |
| CALENDAR_DISPLAY_DAY | 3 | Shows a single day at a time. Better for servers with many events. |
A resource ClusterResources.conductor_host can hand out an exclusive, cluster-wide lock on
via LockClusterResources/
FreeClusterResources.
| Name | Number | Description |
|---|---|---|
| CLUSTER_RESOURCE_BROWSER | 0 | The ability to launch a headless Chrome/Brave browser - see ClusterResources's own doc for why more than one running at once across a cluster's instances can be a problem. |
| CLUSTER_RESOURCE_FFMPEG | 1 | |
| CLUSTER_RESOURCE_IMAGEMAGICK | 2 |
The two contact schemes ContactMethod.value (users.proto) may take --
see ServerConfiguration.supported_contact_protocols for the
server-wide setting keyed off this enum, and docs/contact_integrations.md for the full picture.
| Name | Number | Description |
|---|---|---|
| CONTACT_PROTOCOL_TEL | 0 | tel: (phone/SMS) contact. Requires an enabled TwilioConfig/ BirdConfig/TelnyxConfig. |
| CONTACT_PROTOCOL_MAILTO | 1 | mailto: (email) contact. Currently not supported -- no email provider exists yet. |
The SMS providers ServerConfiguration.preferred_verification_apis/
available_verification_apis order between --
TwilioConfig, BirdConfig, and
TelnyxConfig. See contact_verification.rs's own module doc for the
preference-then-fallback logic these values drive.
| Name | Number | Description |
|---|---|---|
| CONTACT_VERIFICATION_API_TWILIO | 0 | |
| CONTACT_VERIFICATION_API_BIRD | 1 | |
| CONTACT_VERIFICATION_API_TELNYX | 2 |
The default navigation tabs in Rellm's Elm UI.
| Name | Number | Description |
|---|---|---|
| HOME_TAB | 0 | The home/landing tab. |
| EVENTS_TAB | 10 | The Events tab. |
| POSTS_TAB | 11 | The Posts tab. |
| PEOPLE_TAB | 12 | The People tab. |
| ABOUT_TAB | 15 | The About tab. |
| MARKET_TAB | 16 | The Market tab. |
How a nav tab's icon and title are shown together, if at all. Applies uniformly to every tab
(tabs above, or the predefined EVENTS_TAB/POSTS_TAB/PEOPLE_TAB/ABOUT_TAB set when tabs
itself is unset) - there's no per-tab override.
| Name | Number | Description |
|---|---|---|
| NAVIGATION_TAB_ICON_ONLY | 0 | Just the icon/emoji, no visible title (Rellm's original, still-default look). |
| NAVIGATION_TAB_TEXT_ONLY | 1 | Just the title text, no visible icon. |
| NAVIGATION_TAB_ICON_AND_TEXT_BELOW | 2 | Icon above, title below, stacked in one tab. |
| NAVIGATION_TAB_ICON_AND_TEXT_RIGHT | 3 | Icon and title side by side, icon first. |
Strategy when a user sets their visibility to PRIVATE.
| Name | Number | Description |
|---|---|---|
| ACCOUNT_IS_FROZEN | 0 | PRIVATE Users can't see other Users (only PUBLIC_GLOBAL Visilibity Users/Posts/Events). Other users can't see them. |
| LIMITED_CREEPINESS | 1 | Users can see other users they follow, but only PUBLIC_GLOBAL Visilibity Posts/Events. Other users can't see them. |
| LET_ME_CREEP_ON_PPL | 2 | Users can see other users they follow, including their PUBLIC_SERVER Posts/Events. Other users can't see them. |
Offers a choice of web UIs. Generally though, React/Tamagui is a century ahead of Flutter Web, so it's the default.
| Name | Number | Description |
|---|---|---|
| FLUTTER_WEB | 0 | Uses Flutter Web. Loaded from /app. |
| HANDLEBARS_TEMPLATES | 1 | Uses Handlebars templates. Deprecated; will revert to Tamagui UI if chosen. |
| REACT_TAMAGUI | 2 | React UI using Tamagui (a React Native UI library). |
| ELM_SPA | 3 | Uses the Elm SPA client. Loaded from /elm. |
Facebook authentication configuration for the server.
| Field | Type | Label | Description |
|---|---|---|---|
| app_id | string | The Facebook App ID for the server. | |
| app_secret | string | The Facebook App Secret for the server. Never serialized to the client. Admins: Edit this in the database's JSONB column directly. |
Some user on a Rellm server. Most commonly a different server than the one serving up FederatedAccount data, but users may also federate multiple accounts on the same server.
| Field | Type | Label | Description |
|---|---|---|---|
| host | string | The DNS hostname of the server that this user is on. | |
| user_id | string | The user ID of the user on the server. |
A server that this server will federate with.
| Field | Type | Label | Description |
|---|---|---|---|
| host | string | The DNS hostname of the server to federate with. | |
| configured_by_default | bool | optional | Indicates to UI clients that they should enable/configure the indicated server by default. |
| pinned_by_default | bool | optional | Indicates to UI clients that they should pin the indicated server by default (showing its Events and Posts alongside the "main" server). |
The federation configuration for a Rellm server.
| Field | Type | Label | Description |
|---|---|---|---|
| servers | FederatedServer | repeated | A list of servers that this server will federate with. |
| facebook_auth_config | FacebookAuthConfig | optional | Facebook authentication configuration for the server. If set, allows users to create Facebook (and Instagram) SyncDestinations for their Posts and Occasions. |
| x_twitter_auth_config | XTwitterAuthConfig | optional | X (Twitter) authentication configuration for the server. If set, allows users to create X (Twitter) SyncDestinations for their Posts and Occasions - an admin registers one X Developer App here, and every user on the server connects their own X account through it via OAuth, the same relationship facebook_auth_config has to individual Facebook Pages. Until set, XTwitterAccount SyncDestinations always fail with x_twitter_app_not_configured. |
| mastodon_servers | MastodonServer | repeated | Mastodon instances this server has a registered OAuth app on, letting users connect/read their own account on that instance. Unlike Facebook/X, Mastodon has no single central platform to register an app against - every instance is its own separate OAuth authority, so an admin has to register an app on each instance individually before users on it can connect. If a user's instance isn't listed here, clients should surface a "not configured" alert rather than attempting to open an OAuth popup with no app to authorize against. (A client could instead dynamically self-register a throwaway app with the instance directly, via Mastodon's own POST /api/v1/apps, and skip this entirely - Mastodon itself supports that. But that's a client-side choice the Rellm protocol doesn't get involved in either way: this field only covers the admin-pre-registered path, which is what lets an app ID be shown/reused consistently across every client on this server rather than each one self-registering its own.) |
Version information for the Rellm server.
| Field | Type | Label | Description |
|---|---|---|---|
| version | string | The version of the Rellm server. May be suffixed with the GitHub SHA of the commit that generated the binary for the server. |
A Mastodon instance this server has a registered OAuth app on. See FederationInfo.mastodon_servers.
| Field | Type | Label | Description |
|---|---|---|---|
| domain | string | The Mastodon instance's hostname, e.g. "mastodon.social". | |
| app_id | string | The registered app's Client ID for this instance. Safe to serialize to clients - used directly to build the instance's /oauth/authorize URL, the same way FacebookAuthConfig.app_id/ XTwitterAuthConfig.client_id are. |
|
| app_secret | string | The registered app's Client Secret for this instance. Never serialized to the client. Admins: Edit this in the database's JSONB column directly. Used server-side to exchange an authorization code for an access token once a user completes the OAuth popup. | |
| configured_by_default | bool | optional | Indicates to UI clients that they should browse the indicated instance's public timeline by default (added to it with no OAuth/account needed at all - see this message's own doc on the difference between browsing and connecting). |
| pinned_by_default | bool | optional | Indicates to UI clients that they should pin the indicated instance by default (showing its Posts alongside the "main" server). Currently has the same effect as configured_by_default - as of this writing, clients have no "added but not shown" state for a browsed instance the way FederatedServer.pinned_by_default's Server.enabled does, so there's nothing for this to mean in addition to configured_by_default. Kept as its own field for symmetry with FederatedServer, and in case that changes. |
X (Twitter) authentication configuration for the server. See FederationInfo.x_twitter_auth_config.
| Field | Type | Label | Description |
|---|---|---|---|
| client_id | string | The X Developer App's Client ID for the server. | |
| client_secret | string | The X Developer App's Client Secret for the server. Never serialized to the client. Admins: Edit this in the database's JSONB column directly. |
A Bluesky (AT Protocol) account connected as a SyncDestination via an "App Password"
(generated at Settings > App Passwords - not the account's main password), rather than an
OAuth popup.
Media limitation: only attached images on a synced Post/Occasion are posted (up to 4, downloaded and re-uploaded as Bluesky blobs) - video is silently dropped entirely. Bluesky video embeds need a separate, more complex upload-and-processing flow not yet built.
| Field | Type | Label | Description |
|---|---|---|---|
| handle | string | The account's handle, e.g. "jon.bsky.social". | |
| did | string | The account's DID (decentralized identifier), populated by the server when the connection is made. | |
| app_password | string | optional | Only used (and required) on CreateSyncDestination/UpdateSyncDestination: the user's own App Password. Never populated in responses. Sessions are created fresh per post rather than stored/refreshed, since App Passwords don't expire. |
Request to delete a SyncDestination.
| Field | Type | Label | Description |
|---|---|---|---|
| destination | SyncDestination | The destination to be deleted. | |
| delete_synced_posts | bool | Whether to also delete posts already made on the destination (e.g. the Facebook Page posts). |
Request to delete a SyncSource.
| Field | Type | Label | Description |
|---|---|---|---|
| source | SyncSource | The source to be deleted. | |
| delete_synced_events | bool | Whether to delete synced events. |
A Facebook Page connected as a SyncDestination - never a personal profile. Facebook
deprecated the publish_actions permission in 2018, which was the only way any third-party app
could ever post to a personal timeline; there's no Graph API call today, for any app, that can
post anything (feed post, photo, or otherwise) to a personal profile on a user's behalf. A Page
is the only kind of Facebook entity a self-hosted server like this can post to at all - this
isn't a Rellm design choice to work around, it's a hard platform restriction. (Unrelated to
this: Facebook Events specifically are also unreachable, even for Pages - see
docs/facebook_and_x_twitter_federation.md's "It posts to the Page's feed, not a real Facebook
Event" for that separate, independent 2018-era lockdown.)
Media limitation: a synced Post/Occasion's attached video and images are mutually exclusive on Facebook - if both are present, the video is posted and any images are silently dropped (Facebook Pages can't attach both to a single feed post).
| Field | Type | Label | Description |
|---|---|---|---|
| page_id | string | The Facebook Page's ID. | |
| page_name | string | The Facebook Page's name, populated by the server when the connection is made. | |
| short_lived_user_access_token | string | optional | Only used (and required) on CreateSyncDestination: a short-lived user access token from client-side Facebook Login, exchanged server-side for a long-lived Page access token. Never populated in responses. |
Response to a request for the current user's SyncDestinations.
| Field | Type | Label | Description |
|---|---|---|---|
| destinations | SyncDestination | repeated | The current user's SyncDestinations. |
| Field | Type | Label | Description |
|---|---|---|---|
| sources | SyncSource | repeated |
An Instagram Business/Creator account connected as a SyncDestination - never a personal
Instagram account. Unlike FacebookPage's restriction (a deprecated permission that used to let
apps post to a personal timeline), this one was never possible in the first place: Instagram's
Content Publishing API was built from the start only for professional (Business/Creator)
accounts, so a personal Instagram account simply has no API surface to post to at all,
regardless of what this server does. Posting to Instagram also requires the professional account
to be linked to a Facebook Page, so this reuses the same Facebook Login popup and app credentials
as FacebookPage - the server exchanges the token for the Page's access token, then looks up
that Page's linked Instagram Business account.
Media limitation: only the first attached image/video on a synced Post/Occasion is
posted - no carousel/multi-image support yet. A post with no media at all is rejected
(instagram_requires_media) - Instagram's Graph API has no text-only post type.
| Field | Type | Label | Description |
|---|---|---|---|
| instagram_business_account_id | string | The Instagram Business/Creator account's ID, used for all Graph API posting calls. | |
| username | string | The Instagram account's @username, populated by the server when the connection is made. | |
| page_id | string | The linked Facebook Page's ID, kept for reference/reconnect. | |
| short_lived_user_access_token | string | optional | Only used (and required) on CreateSyncDestination: a short-lived user access token from client-side Facebook Login (same flow as FacebookPage), exchanged server-side for a long-lived Page access token, which is also used to post to the linked Instagram account. Never populated in responses. |
A Mastodon account connected as a SyncDestination via a user-supplied Personal Access Token
(generated on the user's own instance, under Preferences > Development), rather than an OAuth
popup - Mastodon instances are user-chosen arbitrary domains, so there's no single app to
register ahead of time the way Facebook/Instagram have one.
Media: up to 4 attached images/videos on a synced Post/Occasion are downloaded and re-uploaded as real Mastodon media attachments (any mix of image/video types); a failed individual upload is skipped rather than failing the whole post.
| Field | Type | Label | Description |
|---|---|---|---|
| instance_host | string | The Mastodon instance's hostname, e.g. "mastodon.social". | |
| username | string | The account's username on that instance, populated by the server when the connection is made. | |
| access_token | string | optional | Only used (and required) on CreateSyncDestination/UpdateSyncDestination: the user's own Personal Access Token for instance_host. Never populated in responses. |
A user-owned destination to sync (cross-post) content out to. Mirrors SyncSource,
but for pushing content out rather than pulling content in. Originally Event-specific
(as EventSyncDestination), now shared by both Occasions (see events.proto's
SyncOccasionRequest) and Posts (see posts.proto's SyncPostRequest).
| Field | Type | Label | Description |
|---|---|---|---|
| id | string | Unique ID for the destination. | |
| owner | Author | The user information for the owner of this destination. | |
| created_at | google.protobuf.Timestamp | The time the SyncDestination was created. | |
| updated_at | google.protobuf.Timestamp | optional | The time the SyncDestination was last updated. |
| synced_occasion_count | uint64 | optional | The number of Occasions synced to this destination so far. Computed with a COUNT at request time (unlike SyncSource's event_count/occasion_count, which are recomputed-and-stored on each sync) since destinations are pushed to on demand, not synced in bulk on an interval. |
| synced_post_count | uint64 | optional | The number of Posts synced to this destination so far. Computed the same way as synced_occasion_count, just against Posts instead of Occasions. |
| facebook_page | FacebookPage | A connected Facebook Page to post Occasions/Posts to. | |
| instagram_account | InstagramAccount | A connected Instagram Business/Creator account to post Occasions/Posts to. | |
| mastodon_account | MastodonAccount | A connected Mastodon account to post Occasions/Posts to. | |
| bluesky_account | BlueskyAccount | A connected Bluesky account to post Occasions/Posts to. | |
| x_twitter_account | XTwitterAccount | A connected X (Twitter) account to post Occasions/Posts to. | |
| threads_account | ThreadsAccount | A connected Threads account to post Occasions/Posts to. |
The status of a single piece of content's (an Occasion or Post) sync (cross-post) to
one SyncDestination. Shared/generic so both Occasion.sync_destinations and
Post.sync_destinations can reuse it.
| Field | Type | Label | Description |
|---|---|---|---|
| sync_destination_id | string | The SyncDestination this status is for. | |
| destination_instance_id | string | optional | The ID of the resulting post on the destination (e.g. a Facebook Post ID). |
| destination_url | string | optional | A link to the resulting post on the destination, if available. |
| synced_at | google.protobuf.Timestamp | optional | The time this content was last successfully synced to the destination. |
A user-owned source to sync events from.
| Field | Type | Label | Description |
|---|---|---|---|
| id | string | Unique ID for the synchronization. | |
| owner | Author | The user information for the owner of this sync source. | |
| sync_interval_seconds | uint64 | How frequently the sync should happen in seconds. | |
| created_at | google.protobuf.Timestamp | The time the SyncSource was created. | |
| updated_at | google.protobuf.Timestamp | optional | The time the SyncSource was last updated. |
| last_synced_at | google.protobuf.Timestamp | optional | The time the SyncSource was last synced. |
| event_count | uint64 | The number of events total associated with this SyncSource. Recomputed on each sync. | |
| occasion_count | uint64 | The number of occasions total associated with this SyncSource. Recomputed on each sync. | |
| post_count | uint64 | The number of posts total associated with this SyncSource. Populated for an RSS/Atom source (recomputed on each sync, like event_count/occasion_count are for an ICS source); always 0 for an ICS source, which syncs Events/Occasions instead. |
|
| ics_subscription_url | string | The iCal subscription URL for the calendar sync. Creates/updates Events/Occasions. | |
| rss_subscription_url | string | The RSS subscription URL for the feed sync. Creates/updates plain Posts. | |
| atom_subscription_url | string | The Atom subscription URL for the feed sync. Creates/updates plain Posts. |
A connected Threads account - a genuinely personal account works fine here, unlike
FacebookPage/InstagramAccount: the Threads API (a separate product from Instagram's,
launched 2024) has no Page-linkage or Business/Creator-account requirement at all - Threads
OAuth directly authorizes whatever single Threads account the user logs in with, personal or
not. It's still a product added to this server's existing Meta App (see FacebookAuthConfig)
rather than a separately-registered app, so no separate auth config is needed. Unlike
FacebookPage/InstagramAccount, connecting one is a response_type=code OAuth flow at
threads.net (not facebook.com) with no "choose a Page" step - the code is exchanged server-side
for a short-lived token, then a long-lived one (~60 day expiry, refreshable via
grant_type=th_refresh_token - not yet implemented; a connected destination will need
reconnecting after ~60 days until a refresh job exists).
Media limitation: only the first attached image/video on a synced Post/Occasion is
posted - no carousel/multi-image support yet. Unlike InstagramAccount, a text-only post
(no media at all) is valid.
| Field | Type | Label | Description |
|---|---|---|---|
| threads_user_id | string | The account's Threads user ID, used for all posting calls. | |
| username | string | The account's @username, populated by the server when the connection is made. | |
| authorization_code | string | optional | Only used (and required) on CreateSyncDestination: the OAuth authorization code from the Threads login popup. Never populated in responses. |
An X (Twitter) account connected as a SyncDestination, via an OAuth 2.0 Authorization Code +
PKCE flow at x.com. Requires this server to have a registered X Developer App configured (see
FederationInfo.x_twitter_auth_config) - every RPC touching an XTwitterAccount destination
fails with x_twitter_app_not_configured until an admin sets one, mirroring
FacebookAuthConfig/facebook_app_not_configured. Unlike Facebook/Instagram/Threads (which reuse one
Meta App), an admin registers this app once and every user on the server connects their own X
account through it - no per-user API keys needed.
Media limitation: up to 4 attached images on a synced Post/Occasion are downloaded and re-uploaded via X's media upload endpoint. Video is not yet supported - X's video upload requires a chunked upload-and-processing flow (mirroring Bluesky's own documented video gap) not yet built; a video attachment is silently skipped.
| Field | Type | Label | Description |
|---|---|---|---|
| username | string | The account's @username, populated by the server when the connection is made. | |
| x_user_id | string | The account's numeric X user ID, populated by the server when the connection is made. | |
| authorization_code | string | optional | Only used (and required) on CreateSyncDestination: the OAuth authorization code from the X login popup. Never populated in responses. |
| code_verifier | string | optional | Only used (and required, alongside authorization_code) on CreateSyncDestination: the PKCE code verifier the popup generated before sending its paired code_challenge to X's authorize endpoint. X mandates PKCE (unlike Threads/Facebook's plain code exchange), so the server needs this to complete the token exchange. Never populated in responses. |
One specific model a user may call right now, and how - via an AIProvider
they own outright (grant unset), or via an AIProviderGrant someone else
granted them (grant set). Only ever defined relative to a user - see
User.ai_models/GetAIProvidersResponse.ai_models.
One AIModel exists per (provider, model) pair: an owner gets one row per model their
provider supports (see the server's own model catalog per provider type); a grantee gets one row
per model their grant actually covers - expanded from AIProviderGrant.model_names, or
every model the provider supports if that list is empty.
| Field | Type | Label | Description |
|---|---|---|---|
| model_name | string | The exact model name to use when calling the provider (e.g. "gemini-3.1-flash-image"). |
|
| capabilities | AIModelCapability | repeated | What this model can actually do - from the server's own hardcoded catalog for provider.provider's variant (see AIModelCapability), not anything reported by the provider's API itself. Feature gating keys off this rather than model_name directly, so e.g. GenerateMedia (which needs AI_MODEL_CAPABILITY_IMAGE_EDITING whenever GenerateMediaRequest.media_ids is non-empty, or just AI_MODEL_CAPABILITY_IMAGE_GENERATION when it's empty) doesn't need its own hardcoded list of model names. |
| grant | AIProviderGrant | optional | The grant that allows this access, when the current user isn't provider.owner themselves. Unset when the current user owns provider outright (full, ungated access - no grant needed). |
| provider | AIProvider | The provider this model belongs to. Its own grants list is only populated when the current user is provider.owner (or an Admin) - see GetAIProviders's own doc; a mere grantee never sees who else has been granted access to a provider they don't own. |
An AIProvider is a user-owned connection to an external AI model API (e.g. a Gemini API
key), which its owner can grant other users of this server metered, budgeted access to. Mirrors
SyncDestination/SyncSource (also user-owned integrations
with an Author owner and a oneof naming which external system is configured), but where
those push/pull content, an AIProvider is metered access to a third-party LLM API - shared out to
other users via AIProviderGrants rather than posted-to/subscribed-from.
Providers are managed via GetAIProviders,
CreateAIProvider (requires CREATE_AI_PROVIDERS, or Admin),
UpdateAIProvider (owner, or Admin for any user's), and
DeleteAIProvider (owner, or Admin) - the same self-or-Admin shape as
SyncDestination's RPCs. Access to a provider is granted/revoked to other users via
GrantAIProvider/RevokeAIProvider which,
unlike every other RPC pair here, are owner-only with no Admin override: an Admin can manage the provider
record itself (rename it, rotate its key, delete it), but handing out access to someone else's API budget is a
call only its owner should be able to make.
GeminiCredentials/OpenAICredentials/
DigitalOceanCredentials all have a working connection flow (Gemini's
Interactions API, OpenAI's Images API, DigitalOcean's Serverless Inference API - the last of which is also
OpenAI-Images-API-shaped, just a different base URL/key and generation-only, no editing endpoint);
AnthropicCredentials is defined for forward compatibility but is not yet
accepted by CreateAIProvider (Anthropic doesn't offer image generation).
| Field | Type | Label | Description |
|---|---|---|---|
| id | string | Unique ID for the AIProvider. | |
| owner | Author | The user information for the owner of this AIProvider - the only user (besides Admins) who may rename it or change its credentials/provider, and the only user (not even Admins) who may grant/revoke other users' access to it. | |
| name | string | A display name for the provider, chosen by its owner (e.g. "My Gemini Key", "Team OpenAI Account"). Purely cosmetic - has no effect on behavior. | |
| gemini_credentials | GeminiCredentials | A Google Gemini API connection, used for image generation/editing (e.g. generating Event posters) via its Interactions API. | |
| openai_credentials | OpenAICredentials | An OpenAI API connection, used for image generation/editing via its Images API (GPT Image models). | |
| anthropic_credentials | AnthropicCredentials | An Anthropic API connection. Not yet creatable - Anthropic doesn't offer an image generation API. | |
| digitalocean_credentials | DigitalOceanCredentials | A DigitalOcean Gradient AI Platform / Serverless Inference connection, used for image generation (no editing - DigitalOcean's Serverless Inference API has no /v1/images/edits-equivalent endpoint) via its OpenAI-Images-API-shaped /v1/images/generations endpoint (GPT Image and Stable Diffusion models, re-hosted under DigitalOcean's own billing). |
|
| grants | AIProviderGrant | repeated | Other users this provider's owner has granted metered access to, via GrantAIProvider. Only ever populated for the owner (or an Admin) -- see GetAIProviders. |
| created_at | google.protobuf.Timestamp | The time the provider was created. | |
| updated_at | google.protobuf.Timestamp | optional | The time the provider was last updated (renamed, or had its provider/credentials changed). |
A grant of metered access to someone else's AIProvider, created/reset via
GrantAIProvider and removed via
RevokeAIProvider. Upserted on the unique
(ai_provider_id, ai_model_grantee) pair - calling GrantAIProvider
again for a user who already has a grant resets tokens_remaining to the newly-requested amount, it does not
add to it.
| Field | Type | Label | Description |
|---|---|---|---|
| ai_provider_id | string | The ID of the AIProvider this grant is for. |
|
| ai_model_grantee | Author | The user this access was granted to. | |
| model_names | string | repeated | The model name (that will be used to call the provider) that the grantee is allowed to use by this grant. If blank, allows access to any models the provider supports. If non-blank, the grantee is only allowed to use the model(s) specified here. Allows granters to set per-model (or per-model-group) token budgets, e.g. "gpt-4" vs "gpt-3.5-turbo". |
| tokens_remaining | uint64 | The number of tokens the grantee may still spend against this provider. Set (and reset) by the owner via GrantAIProvider. Once this reaches 0, GenerateMedia stops working for the grantee entirely, until the owner grants more via GrantAIProvider again. |
|
| overage | uint64 | How far a single GenerateMedia call's actual token usage overshot tokens_remaining the moment it hit 0 - effectively a "negative tokens_remaining" (which, being uint64, can't represent a negative value directly), recorded here instead as a positive debt for the owner's own visibility. E.g. a grantee with 30 tokens left whose next call actually costs 45 ends up with tokens_remaining = 0 and overage = 15. Always 0 immediately after a fresh GrantAIProvider call (any prior debt is cleared, not carried forward) - see that RPC's own doc. |
|
| created_at | google.protobuf.Timestamp | The time the grant was first created. | |
| updated_at | google.protobuf.Timestamp | optional | The time the grant was last updated (i.e. last reset by another GrantAIProvider call). |
Credentials for an Anthropic API connection. Not yet creatable - defined for forward compatibility only.
| Field | Type | Label | Description |
|---|---|---|---|
| anthropic_api_key | string | optional | The Anthropic API key. Never populated in responses (see GeminiCredentials.gemini_api_key). |
Request to delete an AIProvider. Also deletes any of its AIProviderGrants.
| Field | Type | Label | Description |
|---|---|---|---|
| provider | AIProvider | The provider to be deleted. |
Credentials for a DigitalOcean Gradient AI Platform /
Serverless Inference connection, accepted by
CreateAIProvider/UpdateAIProvider.
Used for image generation only (no editing - see AIProvider.provider's own doc on this variant) via its
Serverless Inference API
/v1/images/generations endpoint, OpenAI-Images-API-shaped and re-hosting GPT Image and Stable Diffusion models --
see GenerateMedia.
| Field | Type | Label | Description |
|---|---|---|---|
| digitalocean_api_key | string | optional | The DigitalOcean Serverless Inference API token. Required (and only used) on CreateAIProvider/UpdateAIProvider -- never populated in responses (see GeminiCredentials.gemini_api_key). |
Credentials for a Google Gemini API connection - the only
AIProvider.provider variant currently accepted by
CreateAIProvider/UpdateAIProvider.
Used for image generation/editing via Gemini's Interactions API,
e.g. to generate/edit Event posters from an Event's own content - see GenerateMedia.
| Field | Type | Label | Description |
|---|---|---|---|
| gemini_api_key | string | optional | The Gemini API key. Required (and only used) on CreateAIProvider/UpdateAIProvider -- never populated in responses, the same write-only convention as e.g. MastodonAccount.access_token in sync.proto. |
Request to generate (or edit) an image via one of the current user's
AIModels - see GenerateMedia. The resulting
image is stored as a new Media (generated = true) owned by the current user, and - if
target is set - prepended as the first item in that Post's (or Event's own Post's) media list.
| Field | Type | Label | Description |
|---|---|---|---|
| model | AIModel | Which of the current user's AIModels to generate with - model.model_name selects the actual model, model.provider.id identifies whose AIProvider (the current user's own, or one they've been granted access to) to call it through. Only model_name/provider.id are read server-side - any other field sent here (e.g. a spoofed grant) is ignored in favor of the caller's real access, re-derived from provider.id and the current user. |
|
| user_prompt | string | The user-editable prompt describing what to generate, e.g. "Please generate a square headline poster for the following event." Combined server-side with target's own formatted content (title/description/date-time range/location - the same formatting SyncDestinations use) before being sent to the model, so the user never has to paste that context in by hand. |
|
| media_ids | string | repeated | Existing Media to pass to the model alongside user_prompt, for image editing/ reference-based generation (e.g. a target Post/Event's own current photos), in the order given here. Leave empty for plain text-to-image generation instead - model must have the matching capability either way (AI_MODEL_CAPABILITY_IMAGE_EDITING here, AI_MODEL_CAPABILITY_IMAGE_GENERATION if empty - see AIModelCapability's own doc). Every id must be owned by the current user (or the current user must be an Admin). |
| post_id | string | Attach to (and use the content of) this Post. Caller must be its author, or an Admin. | |
| occasion_id | string | Attach to (and use the content of) this Occasion's parent Event's own Post - named by Occasion, not Event, since that's what a viewer is actually looking at (and what gives the generated prompt its date/time/location context, the same way SyncOccasion does). Caller must be the Event's own Post's author, or hold MODERATE_POSTS/MODERATE_EVENTS, or be an Admin. |
Response to a request for a user's AIProviders.
| Field | Type | Label | Description |
|---|---|---|---|
| providers | AIProvider | repeated | The requested user's own AIProviders (those they own) - exactly the distinct providers in ai_models whose owner is the requested user, each with its own grants populated (who else can use it). A convenience duplicate of data already in ai_models, so callers managing a user's own providers (rename/rekey/delete/grant/ revoke) don't have to de-duplicate that list themselves. |
| ai_models | AIModel | repeated | Every model the requested user may currently call - their own providers' models, plus any models granted to them on other users' providers. See AIModel's own doc. |
Request to grant (or reset) another user's metered access to one of the current user's
AIProviders. Authenticated, owner-only - no Admin override.
| Field | Type | Label | Description |
|---|---|---|---|
| user_id | string | The user to grant access to. | |
| ai_provider_id | string | The AIProvider to grant access to. Must be owned by the caller. | |
| tokens | uint64 | The number of tokens the grantee may spend. Calling this RPC again for the same (ai_provider_id, user_id) pair replaces, rather than adds to, this value. |
|
| model_names | string | repeated | The models the grantee is allowed to use, mirroring AIProviderGrant.model_names - if empty, allows access to any model the provider supports. Also replaced (not merged) on a repeat call, same as tokens. |
Credentials for an OpenAI API connection, accepted by
CreateAIProvider/UpdateAIProvider.
Used for image generation/editing via OpenAI's Images API
(the GPT Image model family) - same use case as GeminiCredentials, see
GenerateMedia.
| Field | Type | Label | Description |
|---|---|---|---|
| openai_api_key | string | optional | The OpenAI API key. Required (and only used) on CreateAIProvider/UpdateAIProvider -- never populated in responses (see GeminiCredentials.gemini_api_key). |
Request to revoke another user's access to one of the current user's
AIProviders, the reverse of
GrantAIProvider. Authenticated, owner-only - no Admin override.
| Field | Type | Label | Description |
|---|---|---|---|
| user_id | string | The user whose access should be revoked. | |
| ai_provider_id | string | The AIProvider to revoke access to. Must be owned by the caller. |
What an AIModel can actually do - drives feature gating
(e.g. GenerateMedia's "Generate Media…" buttons/panel only offer
models carrying AI_MODEL_CAPABILITY_IMAGE_EDITING/AI_MODEL_CAPABILITY_IMAGE_GENERATION)
without the gated feature needing its own hardcoded list of model names to check against. A
model may carry more than one - e.g. an image-editing model can also usually do plain
text-to-image generation.
| Name | Number | Description |
|---|---|---|
| AI_MODEL_CAPABILITY_UNKNOWN | 0 | The model's capabilities are unknown (e.g. the server doesn't know what this provider supports). |
| AI_MODEL_CAPABILITY_TEXT_GENERATION | 1 | The model can generate new text from a prompt. |
| AI_MODEL_CAPABILITY_IMAGE_GENERATION | 2 | The model can generate a new image from a text prompt alone - what GenerateMedia requires when GenerateMediaRequest.media_ids is empty (no reference images to edit with). |
| AI_MODEL_CAPABILITY_IMAGE_EDITING | 3 | The model can edit an existing image, given a text prompt and one or more reference images -- what GenerateMedia requires instead, whenever GenerateMediaRequest.media_ids is non-empty. Not every model with AI_MODEL_CAPABILITY_IMAGE_GENERATION also has this - some (e.g. the cheaper/faster gemini-3.1-flash-lite-image tier) only support plain generation. |
MarketPurchase.details' PURCHASE_TYPE_AI_GRANTS variant -- copied verbatim from the
originating MarketProduct.details at the moment this purchase was fulfilled. Field-for-field
identical to AIGrantSubscriptionDetails -- see MediaStoragePurchaseDetails's own doc for why
it's still a separate message.
| Field | Type | Label | Description |
|---|---|---|---|
| ai_provider_id | string | Which AIProvider this grant is against. |
|
| model_names | string | repeated | Which of that provider's models the grant covers. |
| tokens | uint64 | The buyer's new total token balance for ai_provider_id/model_names, replacing (not adding to) whatever balance remained -- see logic::market_fulfillment::fulfill_purchase's AiGrants arm. |
MarketProduct.details/MarketSubscription.details' PURCHASE_TYPE_AI_GRANTS variant -- what
an AI token product actually grants. Field-for-field identical to AIGrantPurchaseDetails --
see that message's own doc for why it's still a distinct type.
| Field | Type | Label | Description |
|---|---|---|---|
| ai_provider_id | string | Which AIProvider this grant is against. |
|
| model_names | string | repeated | Which of that provider's models the grant covers. |
| tokens | uint64 | How many tokens this product/subscription grants the buyer each time it's (re-)fulfilled, replacing (not adding to) whatever balance remained -- see logic::market_fulfillment::fulfill_purchase's AiGrants arm. |
One entry in a MarketSubscription's fulfillment_notes -- see that field's own doc. Immutable
once appended: UpdateMarketSubscription only ever accepts a fulfillment_notes list whose
existing entries are byte-for-byte identical to what's already stored (see that RPC's own doc).
| Field | Type | Label | Description |
|---|---|---|---|
| user_id | string | Whoever wrote this note -- either the fulfilling admin or the buyer themselves, depending on which side of the conversation this entry is. Must match the id of whoever's actually making the UpdateMarketSubscription request that appends this entry (server-validated -- see that RPC's own doc); a client can't author a note on someone else's behalf. |
|
| note | string | The note's own text -- required (rejected with fulfillment_note_text_required) unless this entry also changes fulfillment_status from whatever the previous entry (or, for the very first note, the implicit FULFILLMENT_STATUS_AWAITING_HOST_ADMIN default) left it at, in which case an admin can record a bare status change with no accompanying text (e.g. the automatic "admin opened this order" transition to FULFILLMENT_STATUS_IN_PROGRESS -- see that enum value's own doc). |
|
| fulfillment_status | FulfillmentStatus | What RellmHostingSubscriptionDetails.fulfillment_status became as of this note -- unchanged from the previous entry for a plain note, or the new value for an actual status transition (see note's own doc on when text is/isn't required for each case). This is what makes fulfillment_notes a genuine "fulfillment state history," not just a chat log alongside a separately-tracked current status. |
|
| created_at | google.protobuf.Timestamp | When this note was added. Server-stamped -- UpdateMarketSubscription always ignores whatever timestamp the client sends for a newly-appended entry. |
Request to get products available for purchase on a Rellm server. Unauthenticated --
GetMarketProducts never requires a signed-in caller (see that RPC's own doc), so anyone can
browse a server's Market without an account, including from another federated server (see
rellm.proto's "Federated Markets" doc). For now, there are few enough products per server that
this has no filtering/paging parameters.
| Field | Type | Label | Description |
|---|---|---|---|
| market_products | MarketProduct | repeated | Every non-delisted MarketProduct, plus delisted ones too if the caller is a signed-in admin on this server (so admins can still find/relist/edit a delisted product from the same /market page everyone else sees). |
Request to get MarketSubscriptions -- self-scoped to the current user's own
(GET_MARKET_SUBSCRIPTIONS_REQUEST_FOR_PURCHASE, the default -- there's no way to fetch another
user's own subscriptions this way, even as an admin), or -- for an admin only --
GET_MARKET_SUBSCRIPTIONS_REQUEST_FOR_FULFILLMENT_ADMIN, every PURCHASE_TYPE_RELLM_HOSTING
subscription across every buyer, for the /market/fulfillment admin page.
| Field | Type | Label | Description |
|---|---|---|---|
| request_type | GetMarketSubscriptionsRequestType | Which of the two views below to return. Defaults to GET_MARKET_SUBSCRIPTIONS_REQUEST_FOR_PURCHASE (proto3's implicit 0 default), so existing callers that predate this field keep getting their own subscriptions, not the admin view. |
| Field | Type | Label | Description |
|---|---|---|---|
| market_subscriptions | MarketSubscription | repeated | For GET_MARKET_SUBSCRIPTIONS_REQUEST_FOR_PURCHASE: the caller's own subscriptions, newest first. For GET_MARKET_SUBSCRIPTIONS_REQUEST_FOR_FULFILLMENT_ADMIN: every PURCHASE_TYPE_RELLM_HOSTING subscription across every buyer, oldest first (so the oldest unfulfilled order surfaces at the top of /market/fulfillment). |
Authenticated*. Buys market_product_id for the current user, starting (or continuing) a
Stripe Checkout flow -- see MakeMarketPurchaseResponse.checkout_url. No MarketPurchase/
MarketSubscription is created by this call itself; that only happens once Stripe confirms
payment via webhook, so an abandoned checkout never leaves a half-created purchase behind.
Rejected outright (market_disabled) if market_settings.enabled is false -- checked first,
ahead of every product-specific precondition (delisted/sold-out/Stripe-not-configured/etc.),
since an admin who's closed Market entirely shouldn't have that bypassable by simply knowing a
still-valid product id.
| Field | Type | Label | Description |
|---|---|---|---|
| market_product_id | string | ||
| rellm_hosting_details | RellmHostingPurchaseDetails | optional | Only meaningful (and required) for a PURCHASE_TYPE_RELLM_HOSTING product -- the buyer's desired domain/contact info/notes, carried through to Stripe as Checkout Session metadata and copied onto the resulting MarketPurchase/MarketSubscription once payment completes. |
A pending Stripe Checkout Session, ready to redirect the buyer's browser to.
| Field | Type | Label | Description |
|---|---|---|---|
| checkout_url | string | Redirect the buyer's browser here (a Stripe-hosted Checkout page) to complete payment. Expires after Stripe's own Checkout Session timeout if never completed -- since no MarketPurchase row is created until the webhook fires (see this message's own request's doc), an abandoned/expired checkout leaves no trace at all. |
A single successful charge against a MarketPurchase -- one row per completed Stripe
PaymentIntent (the initial purchase's, or a later renewal's). Backed by the same
market_payments table as MarketRefund (a positive amount row marshals to a MarketPayment,
a negative one to a MarketRefund -- see that message's own doc), so a MarketPayment is never
itself edited or deleted once created; a refund is always its own separate row/message.
| Field | Type | Label | Description |
|---|---|---|---|
| amount | uint32 | The amount actually charged, in the same unit MarketProduct.amount uses (smallest unit of currency, except for a zero-decimal currency like JPY). |
|
| currency | uint32 | The ISO 4217 numeric currency code amount is denominated in -- copied from the purchase's own product at charge time. |
|
| market_purchase_id | string | ||
| method | MarketPaymentMethod | optional | The card actually charged, if known/resolvable at the time this MarketPayment was recorded. |
| created_at | google.protobuf.Timestamp | When this charge succeeded. |
Card details for a MarketPayment, resolved from Stripe at charge time (Stripe's own
PaymentMethod.card object). Unset entirely if the payment wasn't card-based or details
couldn't be resolved. Never carries anything more sensitive than what Stripe itself considers
safe to display (brand/last4/expiry) -- never a full card number.
| Field | Type | Label | Description |
|---|---|---|---|
| card_brand | string | E.g. "visa", "mastercard", "amex". | |
| card_last4 | string | Last 4 digits of the card number. | |
| card_exp_month | uint32 | 1-12. | |
| card_exp_year | uint32 | 4-digit year. |
An actual subscribable product listed on, say, https://rellm.org/market -- what an admin creates
via CreateMarketProduct/edits via UpdateMarketProduct, and what a buyer actually purchases via
MakeMarketPurchase. market_settings.enabled (see server_configuration.proto) gates whether a
server's Market is browsable/purchasable at all, independent of any individual product's own
delisted_at. Listed/delisted by clients by setting delisted_at (though the actual date
supplied by the client is ignored -- see that field's own doc).
| Field | Type | Label | Description |
|---|---|---|---|
| id | string | ||
| type | PurchaseType | What this product grants once purchased -- see PurchaseType's own doc for what each value does. Never changeable after product creation (UpdateMarketProduct silently ignores any change to this field) -- changing what a product is after people have already bought it would silently change existing buyers' entitlements out from under them; a product whose type needs to change is delisted and replaced with a new one instead. |
|
| period | PurchasePeriod | How often this product bills, if at all -- see PurchasePeriod's own doc. Never changeable after product creation, same reasoning as type above. |
|
| amount | uint32 | The price, in the smallest unit of currency (e.g. cents for USD) -- except for a zero-decimal currency like JPY, where this is already the whole unit (see logic::stripe_sync::is_zero_decimal_currency). |
|
| currency | uint32 | The ISO 4217 numeric currency code this product is priced in (e.g. 840 for USD, 392 for JPY) -- see logic::market_summary's currency table for the full set of currencies a server actually supports pricing in today. |
|
| available_count | uint32 | Number of subscription "slots" available for this product (admin-set) -- 0 means unlimited. Once sold_count >= available_count (and available_count > 0), MakeMarketPurchase rejects further purchases with product_sold_out. |
|
| sold_count | uint32 | Number of subscriptions actually sold, maintained server-side (never client-settable -- UpdateMarketProduct silently ignores any client-sent value for this field). Incremented when a purchase's Stripe Checkout Session completes; decremented when the resulting MarketSubscription is actually canceled (CancelMarketSubscription), freeing the slot for a new buyer. |
|
| media_storage_subscription_details | MediaStorageSubscriptionDetails | ||
| ai_grant_subscription_details | AIGrantSubscriptionDetails | ||
| rellm_hosting_subscription_details | RellmHostingSubscriptionDetails | ||
| permissions_access_subscription_details | PermissionsAccessSubscriptionDetails | ||
| created_at | google.protobuf.Timestamp | When this product was created. Server-stamped -- CreateMarketProduct ignores any client-sent value. |
|
| delisted_at | google.protobuf.Timestamp | optional | If set, the MarketProduct is not purchasable -- still shown to admins (see GetMarketProductsResponse.market_products' own doc), but hidden from every other caller and rejected by MakeMarketPurchase. Note: clients toggle listings by setting this, but the server will always set it to the time of the request, not the time sent by the request. |
One completed billing event -- the initial purchase or a later recurring renewal charge -- for a
single product. Created only from web::stripe_webhook (the initial purchase, on
checkout.session.completed) or logic::market_renewal (each subsequent recurring charge),
never directly by MakeMarketPurchase itself (see that RPC's own doc). MarketPurchases are
immutable via the API+CLI once created -- there is no UpdateMarketPurchase RPC; the payments,
refunds, and (for a subscription) fulfillment information that accumulate against a purchase over
time live in their own separate messages/tables instead of ever rewriting this one.
| Field | Type | Label | Description |
|---|---|---|---|
| id | string | ||
| buyer | Author | Who bought this. | |
| type | PurchaseType | What this purchase grants -- copied from (and always matching) market_product.type at the time of purchase. Denormalized here (rather than requiring a lookup through market_product) so a client can branch on details' oneof case without needing market_product populated. |
|
| market_product | MarketProduct | The MarketProduct this purchase was made against, as it existed at the time it was fetched -- may since have changed price/details/been delisted; this purchase's own amount-equivalent fields live on whichever MarketPayments are attached, not here. |
|
| market_subscription | MarketSubscription | optional | The subscription this purchase belongs to -- every purchase gets one, including a PURCHASE_PERIOD_INDEFINITE one-time purchase (see MarketSubscription's own doc), so Optional here really only means "always unset when this MarketPurchase is itself embedded inside a MarketSubscription.billing_history" (there'd be no point recursing into the same subscription again). Note: this circular relationship should be handled by the Rust marshaling side. |
| market_payments | MarketPayment | repeated | Every payment recorded against this purchase, oldest first -- ordinarily just one, but a failed charge that's later retried (see logic::market_renewal) can leave more than one row. |
| market_refunds | MarketRefund | repeated | Every refund recorded against this purchase, oldest first -- empty for the common case of a purchase that was never refunded. |
| media_storage_purchase_details | MediaStoragePurchaseDetails | ||
| ai_grant_purchase_details | AIGrantPurchaseDetails | ||
| rellm_hosting_purchase_details | RellmHostingPurchaseDetails | ||
| permissions_access_purchase_details | PermissionsAccessPurchaseDetails | ||
| created_at | google.protobuf.Timestamp | When this purchase was recorded -- i.e. when the Stripe webhook/renewal job actually processed it, not when the buyer started checkout. |
A single refund issued against a MarketPurchase's payment -- see MarketPayment's own doc for
how this and MarketPayment share the same underlying market_payments table.
| Field | Type | Label | Description |
|---|---|---|---|
| amount | uint32 | The amount refunded, in the same unit the original MarketPayment.amount was charged in -- always positive here (the underlying row's negative amount is what distinguishes a refund from a payment; this message itself never exposes the sign). |
|
| currency | uint32 | The ISO 4217 numeric currency code amount is denominated in -- always matches the MarketPayment.currency being refunded. |
|
| market_purchase_id | string | ||
| method | MarketRefundMethod | optional | The card the refund was issued back to -- in practice always the same card as the MarketPayment being refunded, since Stripe refunds are only ever issued back to their original payment method. |
| created_at | google.protobuf.Timestamp | When this refund was issued. |
Same shape as MarketPaymentMethod -- kept as its own message (rather than reusing MarketPaymentMethod directly) since a MarketRefund and the MarketPayment it refunds are otherwise-independent messages, matching the MarketPayment/MarketRefund split itself.
| Field | Type | Label | Description |
|---|---|---|---|
| card_brand | string | E.g. "visa", "mastercard", "amex". | |
| card_last4 | string | Last 4 digits of the card number. | |
| card_exp_month | uint32 | 1-12. | |
| card_exp_year | uint32 | 4-digit year. |
Created for every MarketPurchase, regardless of MarketProduct.period -- a recurring
(PURCHASE_PERIOD_ANNUAL/PURCHASE_PERIOD_MONTHLY) one gets re-billed and re-fulfilled
automatically every period by renew_market_subscriptions.rs until canceled; a
PURCHASE_PERIOD_INDEFINITE one-time purchase gets a subscription too (with renews_at unset --
see that field's own doc), purely so it's still cancelable and still carries the same per-type
fulfillment tracking (e.g. RellmHostingSubscriptionDetails.fulfillment_status/fulfillment_notes) every
other purchase type gets, even though it never actually bills again.
| Field | Type | Label | Description |
|---|---|---|---|
| id | string | ||
| buyer | Author | Who owns this subscription (i.e. who's being billed and who the entitlement applies to). | |
| type | PurchaseType | What this subscription grants -- copied from (and always matching) market_product.type. Denormalized here the same way MarketPurchase.type is -- see that field's own doc. |
|
| period | PurchasePeriod | How often this subscription bills -- copied from market_product.period at the time this subscription was created. PURCHASE_PERIOD_INDEFINITE is valid here too (see this message's own doc) -- it just means renews_at stays unset and this subscription never actually bills again. |
|
| amount | uint32 | The price charged each renewal, in the same unit MarketProduct.amount uses -- copied from market_product.amount at the time this subscription was created, so a later price change to the product doesn't retroactively re-price an existing subscriber. |
|
| currency | uint32 | The ISO 4217 numeric currency code amount is denominated in -- copied from market_product.currency at the time this subscription was created. |
|
| market_product | MarketProduct | The MarketProduct this subscription was made against, as it existed at the time it was fetched -- may since have changed price/details/been delisted (delisting an already-subscribed product doesn't cancel existing subscriptions, only blocks new purchases). Note: marshaling should handle the circular relationship here gracefully. |
|
| billing_history | MarketPurchase | repeated | Every MarketPurchase billed against this subscription so far -- the original purchase plus every successful renewal charge, newest first. Each entry's own market_subscription field is left unset here (see MarketPurchase.market_subscription's own doc) to avoid recursing back into this same subscription. |
| media_storage_subscription_details | MediaStorageSubscriptionDetails | ||
| ai_grant_subscription_details | AIGrantSubscriptionDetails | ||
| rellm_hosting_subscription_details | RellmHostingSubscriptionDetails | ||
| permissions_access_subscription_details | PermissionsAccessSubscriptionDetails | ||
| created_at | google.protobuf.Timestamp | When this subscription was first created (i.e. when the initial MarketPurchase was fulfilled). |
|
| renews_at | google.protobuf.Timestamp | optional | When the next renewal charge is due. Always unset for a PURCHASE_PERIOD_INDEFINITE subscription (see this message's own doc) -- there is no next charge. Otherwise, advanced by one period on every successful renewal (renew_market_subscriptions.rs); left untouched once canceled_at is set, since a canceled subscription never renews again regardless of what this still says. |
| canceled_at | google.protobuf.Timestamp | optional | Set once the subscription will no longer renew -- either the buyer/admin explicitly canceled it (CancelMarketSubscription) or a renewal charge failed. The subscription's entitlement (media storage quota, granted permissions, etc.) stays active until whichever is later of renews_at/canceled_at, at which point renew_market_subscriptions.rs revokes it and sets service_terminated_at. Also the moment MarketProduct.sold_count is decremented, freeing this subscription's slot for a new buyer (see that field's own doc). |
| service_terminated_at | google.protobuf.Timestamp | optional | The time permissions were removed, media storage quotas reset, etc. -- i.e. when logic::market_fulfillment::terminate_entitlement actually ran for this subscription. Always unset while canceled_at is unset; may remain unset for a while after canceled_at is set, since the entitlement intentionally stays active until the later of renews_at/canceled_at (see canceled_at's own doc) -- a buyer who cancels mid-period keeps what they already paid for through the end of that period. |
MarketPurchase.details' PURCHASE_TYPE_MEDIA_STORAGE variant -- copied verbatim from the
originating MarketProduct.details at the moment this purchase was fulfilled (see
MarketPurchase.details' own doc). Field-for-field identical to
MediaStorageSubscriptionDetails -- kept as its own message only so the Purchase- and
Subscription-side oneofs stay independent Rust types (see
logic::market_fulfillment::terminate_entitlement's own doc for why that distinction matters
for PermissionsAccessPurchaseDetails/PermissionsAccessSubscriptionDetails).
| Field | Type | Label | Description |
|---|---|---|---|
| allocation_bytes | uint64 | The buyer's new total media storage allocation, replacing (not adding to) whatever quota they already had -- see logic::market_fulfillment::fulfill_purchase's MediaStorage arm. |
MarketProduct.details/MarketSubscription.details' PURCHASE_TYPE_MEDIA_STORAGE variant --
what a media storage product actually grants. Field-for-field identical to
MediaStoragePurchaseDetails -- see that message's own doc for why it's still a distinct type.
| Field | Type | Label | Description |
|---|---|---|---|
| allocation_bytes | uint64 | How much media storage this product/subscription grants the buyer, replacing (not adding to) whatever quota they already had -- see logic::market_fulfillment::fulfill_purchase's MediaStorage arm. |
MarketPurchase.details' PURCHASE_TYPE_PERMISSIONS_ACCESS variant -- copied verbatim from the
originating MarketProduct.details at the moment this purchase was fulfilled. Field-for-field
identical to PermissionsAccessSubscriptionDetails, but kept as a genuinely distinct Rust type
(not just documentation) -- see logic::market_fulfillment::terminate_entitlement's own doc,
which parses a MarketSubscription's details as PermissionsAccessSubscriptionDetails
specifically (never this message) when clawing back a lapsed grant.
| Field | Type | Label | Description |
|---|---|---|---|
| permissions | Permission | repeated | The permissions this purchase granted -- see logic::market_fulfillment::fulfill_purchase's PermissionsAccess arm (adds these to the buyer's User.permissions, union-style). |
| name | string | Admin-authored product name shown for this purchase (e.g. on /market/fulfillment's billing history) -- copied from PermissionsAccessSubscriptionDetails.name at the moment this purchase was fulfilled. Unlike the other three purchase-detail messages' implicit, Elm-computed display names, permissions-access products have no fixed bundle of permissions to describe generically, so an admin names/describes each one by hand. |
|
| description | string | Admin-authored, Markdown-formatted product description -- copied from PermissionsAccessSubscriptionDetails.description the same way name above is. |
MarketProduct.details/MarketSubscription.details' PURCHASE_TYPE_PERMISSIONS_ACCESS
variant -- what a permissions-bundle product actually grants. Field-for-field identical to
PermissionsAccessPurchaseDetails -- see that message's own doc for why it's still a distinct
type (that distinction is exactly what lets logic::market_fulfillment::terminate_entitlement
tell "what to claw back" apart from "what was originally billed").
| Field | Type | Label | Description |
|---|---|---|---|
| permissions | Permission | repeated | Which Permissions this product/subscription grants the buyer -- see logic::market_fulfillment::fulfill_purchase's PermissionsAccess arm (union-added to the buyer's own User.permissions, never replacing what they already had) and terminate_entitlement's own arm (the exact claw-back set on cancellation/expiry). Intentionally excludes permissions dangerous or nonsensical to sell this way -- e.g. "Grant Basic Permissions," any "Moderate"/"Read All System Messages" permission, "Admin," "View Private Contact Methods," and "Edit Cluster Settings" must never appear in a Market product's own permissions list. Enforced server-side on CreateMarketProduct/ UpdateMarketProduct (rejected with permission_not_purchasable) and again on MakeMarketPurchase (defense in depth, in case a permission is later removed from the purchasable set after a product granting it already exists) -- see rpcs::market::create_market_product::PURCHASABLE_PERMISSIONS. NOTE: that Rust list is an explicit include-list, not an exclude-list -- described here as an exclusion for readability, but implemented as "only these permissions are purchasable" so a newly-added Permission is never purchasable by default; it has to be deliberately added to that list. |
| name | string | Admin-authored product name -- unlike MediaStorageSubscriptionDetails/AIGrantSubscriptionDetails/ RellmHostingSubscriptionDetails (which get an implicit, Elm-computed display name from their own fields, since they each describe one fixed kind of thing), a permissions-access product's permissions list can be any admin-chosen bundle, so there's no generic way to name it automatically. Required for a purchasable product (CreateMarketProduct/UpdateMarketProduct reject a PermissionsAccessSubscriptionDetails with a blank name). On a MarketSubscription: copied from the originating MarketProduct.details.name at the time the subscription was created, same as every other field on this message. |
|
| description | string | Admin-authored, Markdown-formatted product description shown on the product's own page -- same "no generic implicit description" reasoning as name above. On a MarketSubscription: copied the same way name is. |
MarketPurchase.details' PURCHASE_TYPE_RELLM_HOSTING variant -- unlike the other three
*PurchaseDetails messages, NOT copied from the originating MarketProduct.details; instead
built fresh at checkout time from the buyer's own MakeMarketPurchaseRequest.rellm_hosting_details
(carried through as Stripe Checkout Session metadata -- see rpcs::market::make_market_purchase
and web::stripe_webhook::handle_checkout_session_completed). Field-for-field identical to
RellmHostingSubscriptionDetails (minus that message's fulfillment_status/fulfillment_notes) -- see
MediaStoragePurchaseDetails's own doc for why it's still a separate message.
| Field | Type | Label | Description |
|---|---|---|---|
| db_size_bytes | uint64 | NOTE: as of the current webhook implementation, the buyer never supplies this and the originating MarketProduct's own configured size isn't carried through Checkout Session metadata either, so this is currently always 0 here -- an admin fulfilling an order today needs to cross-reference the MarketProduct itself for the size actually sold. Intended to be the requested PostgreSQL database size in bytes. |
|
| minio_size_bytes | uint64 | Same caveat as db_size_bytes above -- currently always 0. Intended to be the requested MinIO (object storage) size in bytes. |
|
| additional_description | string | Copied from RellmHostingSubscriptionDetails.additional_description at the moment this purchase was fulfilled -- see that field's own doc. |
|
| domain | string | The domain the buyer wants their new Rellm instance reachable at (e.g. "myserver.example.com"). | |
| contact_email | string | Where the fulfilling admin should reach the buyer about this order, separate from whatever email/contact info is on the buyer's own User (which may not be checked as often, or may not exist at all for a server with no email-based signup). |
|
| additional_information | string | Free-form notes from the buyer to the fulfilling admin, captured once at purchase time (e.g. special requests, existing-data-migration needs). Immutable after purchase -- see RellmHostingSubscriptionDetails.additional_information's own doc, which carries this same text forward onto the resulting MarketSubscription. |
MarketProduct.details/MarketSubscription.details' PURCHASE_TYPE_RELLM_HOSTING variant --
what a dedicated-hosting product actually grants, plus the buyer's own request details and the
admin's own fulfillment tracking for it. Field-for-field identical to RellmHostingPurchaseDetails
for the first five fields (see that message's own doc); fulfillment_status/fulfillment_notes below have
no *PurchaseDetails counterpart, since they're only ever meaningful on the standing
MarketSubscription, not on any one individual MarketPurchase billing event.
| Field | Type | Label | Description |
|---|---|---|---|
| db_size_bytes | uint64 | On a MarketProduct: the PostgreSQL database size (in bytes) this product is configured to provision. On a MarketSubscription: see RellmHostingPurchaseDetails.db_size_bytes's own doc -- as of the current webhook implementation, this is currently always 0 here too, since the subscription's details is built the same way the purchase's is. |
|
| minio_size_bytes | uint64 | Same caveat as db_size_bytes above. On a MarketProduct: the MinIO (object storage) size (in bytes) this product is configured to provision. |
|
| additional_description | string | Admin-authored, Markdown-formatted extra paragraph appended below the implicit, Elm-computed "1GB DB + 5GB Object Storage"-style canned description shown on the product/subscription's own page -- e.g. to call out something specific to this hosting tier that the canned text doesn't cover. Optional; the canned description alone is shown when this is blank. | |
| domain | string | On a MarketProduct: unset/meaningless (a product isn't tied to any one domain). On a MarketSubscription: the domain the buyer wants their new Rellm instance reachable at, from RellmHostingPurchaseDetails.domain. |
|
| contact_email | string | On a MarketProduct: unset/meaningless. On a MarketSubscription: where the fulfilling admin should reach the buyer about this order, from RellmHostingPurchaseDetails.contact_email. |
|
| additional_information | string | Immutable after purchase -- the buyer's own notes to the admin fulfilling this order. Never editable via UpdateMarketSubscription (see that RPC's own doc); fulfillment_notes below is the admin/buyer conversation about fulfilling it. |
|
| fulfillment_status | FulfillmentStatus | Where this Rellm hosting order currently stands -- Rellm hosting is deliberately not automated (see market.proto's own top-of-file notes and logic::market_fulfillment::fulfill_purchase's RellmHosting no-op arm), so this is the one manual "how far along is this order" signal, shown on /market/fulfillment (GET_MARKET_SUBSCRIPTIONS_REQUEST_FOR_FULFILLMENT_ADMIN). Never independently settable by a client -- always server-derived as whatever fulfillment_notes' own last entry's fulfillment_status says (or FULFILLMENT_STATUS_AWAITING_HOST_ADMIN if fulfillment_notes is empty), so this field can never drift out of sync with the history that explains why it's in that state. |
|
| fulfillment_notes | FulfillmentNote | repeated | The admin/buyer conversation about fulfilling this order -- oldest to newest, append-only (see UpdateMarketSubscription's own doc: a new entry can only ever be appended after whatever's already here, never inserted/reordered/removed, and its user_id must match whoever's actually making the request -- the server stamps created_at itself). |
The state of a PURCHASE_TYPE_RELLM_HOSTING order's manual fulfillment -- see
RellmHostingSubscriptionDetails.fulfillment_status's own doc for how the "current" value is
derived, and FulfillmentNote.fulfillment_status for how every transition is recorded as its own
timestamped note (a "fulfillment state history"), not just tracked as a bare current value.
| Name | Number | Description |
|---|---|---|
| FULFILLMENT_STATUS_AWAITING_HOST_ADMIN | 0 | The starting state for every new order -- no admin has looked at it yet. |
| FULFILLMENT_STATUS_FULFILLED | 1 | The order is fully stood up -- set via "Add and Mark as Fulfilled" on /market/fulfillment (UpdateMarketSubscription). |
| FULFILLMENT_STATUS_IN_PROGRESS | 2 | An admin has started working the order but it isn't done yet -- set automatically the first time an admin expands this order's row on /market/fulfillment (if it was still FULFILLMENT_STATUS_AWAITING_HOST_ADMIN), or explicitly via a note. |
| Name | Number | Description |
|---|---|---|
| GET_MARKET_SUBSCRIPTIONS_REQUEST_FOR_PURCHASE | 0 | The "user facing" view backing /market -- the caller's own MarketSubscriptions only. |
| GET_MARKET_SUBSCRIPTIONS_REQUEST_FOR_FULFILLMENT_ADMIN | 1 | Admin-only view backing /market/fulfillment -- every PURCHASE_TYPE_RELLM_HOSTING MarketSubscription across every buyer, since Rellm hosting needs manual setup that isn't automated (see RellmHostingSubscriptionDetails.fulfillment_status's own doc). |
How often a MarketProduct/MarketSubscription bills. Immutable on a MarketProduct once
created (see that message's own doc) -- same reasoning as PurchaseType's immutability.
| Name | Number | Description |
|---|---|---|
| PURCHASE_PERIOD_INDEFINITE | 0 | A single one-time purchase -- still creates a MarketSubscription alongside its MarketPurchase (see MarketSubscription's own doc), so it's still cancelable and still carries per-type fulfillment tracking (e.g. RellmHostingSubscriptionDetails.fulfillment_status/ fulfillment_notes) the same way a recurring one does -- it just has renews_at unset and never bills again. Canceling one takes effect immediately (see MarketSubscription.canceled_at's own doc: entitlement stays active until the later of renews_at/canceled_at, and an unset renews_at is never later than anything). |
| PURCHASE_PERIOD_ANNUAL | 1 | Renews (re-bills and re-fulfills) once per year, via renew_market_subscriptions.rs. |
| PURCHASE_PERIOD_MONTHLY | 2 | Renews (re-bills and re-fulfills) once per month, via renew_market_subscriptions.rs. |
What a MarketProduct/MarketPurchase/MarketSubscription actually grants the buyer once
fulfilled -- see logic::market_fulfillment::fulfill_purchase (the Rust match on this same enum)
for exactly what each value does. Immutable on a MarketProduct once created (see that message's
own doc) -- changing what a product is after people have already bought it would silently
change existing buyers' entitlements out from under them, so a product whose type needs to change
is delisted and replaced with a new one instead.
| Name | Number | Description |
|---|---|---|
| PURCHASE_TYPE_MEDIA_STORAGE | 0 | Extra media storage allocation -- fulfillment sets the buyer's User.media_storage_limit_bytes to MediaStoragePurchaseDetails.allocation_bytes outright (not additive with any existing quota). On cancellation/expiry, reverts to the server's current configured default allocation (ServerConfiguration.media_settings.default_media_allocation_bytes), not to unlimited. |
| PURCHASE_TYPE_AI_GRANTS | 1 | AI provider token grants -- fulfillment resets (never adds to) the buyer's AIProviderGrant.tokens_remaining for AIGrantPurchaseDetails.ai_provider_id/model_names to AIGrantPurchaseDetails.tokens, same "reset, don't add" semantics every renewal uses. Not automatically revoked on cancellation/expiry -- whatever tokens remain when the subscription lapses just aren't replenished again. |
| PURCHASE_TYPE_RELLM_HOSTING | 2 | A dedicated Rellm server instance, hosted and administered by Jon. Deliberately NOT automated -- fulfillment applies no entitlement at all; an admin provisions the server by hand and tracks progress via RellmHostingSubscriptionDetails.fulfillment_status/fulfillment_notes on the /market/fulfillment admin page. Not automatically revoked on cancellation/expiry either (out of scope for this MVP -- an admin handles teardown manually too). |
| PURCHASE_TYPE_PERMISSIONS_ACCESS | 3 | A bundle of Permissions (e.g. SYNC_EVENTS_TO_FACEBOOK) granted directly to the buyer's own User.permissions, union-style -- fulfillment only ever adds permissions the buyer doesn't already have from some other source, never removes any. Unlike the other three types, this ONE eventually claws back what it granted: once cancellation/expiry actually takes effect (see MarketSubscription.canceled_at/service_terminated_at), logic::market_fulfillment:: terminate_entitlement removes exactly the permissions this subscription granted (a plain set difference, not a reconciliation against any other subscription/grant the buyer might also hold). |
| .proto Type | Notes | C++ | Java | Python | Go | C# | PHP | Ruby |
|---|---|---|---|---|---|---|---|---|
| double | double | double | float | float64 | double | float | Float | |
| float | float | float | float | float32 | float | float | Float | |
| int32 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) |
| int64 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. | int64 | long | int/long | int64 | long | integer/string | Bignum |
| uint32 | Uses variable-length encoding. | uint32 | int | int/long | uint32 | uint | integer | Bignum or Fixnum (as required) |
| uint64 | Uses variable-length encoding. | uint64 | long | int/long | uint64 | ulong | integer/string | Bignum or Fixnum (as required) |
| sint32 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) |
| sint64 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s. | int64 | long | int/long | int64 | long | integer/string | Bignum |
| fixed32 | Always four bytes. More efficient than uint32 if values are often greater than 2^28. | uint32 | int | int | uint32 | uint | integer | Bignum or Fixnum (as required) |
| fixed64 | Always eight bytes. More efficient than uint64 if values are often greater than 2^56. | uint64 | long | int/long | uint64 | ulong | integer/string | Bignum |
| sfixed32 | Always four bytes. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) |
| sfixed64 | Always eight bytes. | int64 | long | int/long | int64 | long | integer/string | Bignum |
| bool | bool | boolean | boolean | bool | bool | boolean | TrueClass/FalseClass | |
| string | A string must always contain UTF-8 encoded or 7-bit ASCII text. | string | String | str/unicode | string | string | string | String (UTF-8) |
| bytes | May contain any arbitrary sequence of bytes. | string | ByteString | str | []byte | ByteString | string | String (ASCII-8BIT) |