Get desktop application:
View/edit binary Protocol Buffers messages
[Jump to the gRPC API?](#grpc-api) [Rellm](https://github.com/JonLatane/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](#2-minute-startup-with-homebrew) and [3 minute setup on Linux](#3-minute-startup-on-linux), [images](https://hub.docker.com/r/jonlatane/rellm/tags) on [DockerHub](https://hub.docker.com/r/jonlatane/rellm_preview_generator/tags) and deployment to your K8s clusters available via a simple but powerful `Makefile`-based design language. ### Ports & Protocols Rellm servers interact across several ports: * gRPC (27707) - The main [Rellm gRPC API](#grpc-api). This is the primary port for all Rellm clients. It may or may not be TLS-enabled (443). * Clients are expected to negotiate the gRPC host via the [`backend_host` HTTP endpoint (see below)](#http-based-client-host-negotiation-for-external-cdns) on port 80/443. * HTTP (80, 8000, 27705), HTTPS (443) - The main Rellm HTTP API. This is used for some endpoints, including media upload/download, and for negotiating the gRPC host. * [Public HTTP Endpoints](#external-http-servers-80-8000-443) and the Elm, React and Flutter [Web UI](#web-ui-paths)s are served up on multiple ports: * Port 443 will serve up a secure HTTPS server. If it fails to startup, Rellm handles this gracefully and degrades to plain HTTP. * Port 80 will serve up either an unsecured set of Rellm's HTTP endpoints, or a redirect to the HTTPS/443 server if that one launched successfully. * Port 8000 *always* serves up an unsecured Rellm UI, in case something goes horribly wrong with 80 and 443. It can probably not be exposed in your load balancer/to the web. * Port 27705 is a [private, unsecured HTTP server](#internal-http-server-27705) meant for communication with other non-web facing services on your computer or in your cluster. It should not be exposed to the web. * Currently this just has an `/email` endpoint. It is designed for [email/SMTP support via an integration with Stalwart](https://github.com/JonLatane/rellm/tree/main/deploys/email). #### Cross-Protocol Federation Rellm clients can translate content from other federated protocols into the same [`Post`](#rellm-Post)/[`Author`](#rellm-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`](#rellm-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`](#rellm-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`](#rellm-Post) *to* Mastodon or Bluesky is a separate feature, [`SyncDestination`](#rellm-SyncDestination). ##### Mastodon/ActivityPub 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`](#rellm-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`](#rellm-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. ##### BlueSky/AT Protocol AT Protocol splits "a server" into roles: a Personal Data Server (PDS) hosts an account's data and handles its login, while an AppView serves the read side (feeds, threads, profiles) for the whole network. Like Mastodon's public timelines, a client can browse public Bluesky data with no account at all by reading from an AppView such as `public.api.bsky.app` (unauthenticated, CORS-enabled) - an admin can recommend one via [`BlueskyServer`](#rellm-BlueskyServer) (`FederationInfo.bluesky_servers`). What an anonymous reader can't get is anything personalized, like a home timeline. Connecting a Bluesky *account* additionally unlocks that: a handle and an [App Password](https://bsky.app/settings/app-passwords) (not OAuth -- AT Protocol has no per-client app-registration step the way Mastodon/Facebook/X require), used to call `com.atproto.server.createSession` on the account's *own* PDS (found by resolving the handle to a DID and reading the DID document's `#atproto_pds` service - it isn't necessarily `bsky.social`) and then the account's own `app.bsky.feed.getTimeline`. Because there's no server-side app to register, nothing about connecting an account is admin-configurable the way a Mastodon OAuth app is. Clients show at most one Bluesky source at a time (an AppView *or* an account), since both read the same global network. ### API Design Notes #### Moderation and Visibility Rellm APIs are designed to support [`Moderation`](#rellm-Moderation) and [`Visibility`](#rellm-Visibility) controls at the level of individual entities. However, to keep things DRY, moderation and visibility controls are only implemented for [`User`](#rellm-User)s, [`Media`](#rellm-Media), [`Group`](#rellm-Group)s, and [`Post`](#rellm-Post)s. [`Event`](#rellm-Event)s and future [`Post`](#rellm-Post)-like types simply use the same implementation as their contained [`Post`](#rellm-Post)s. The intent here is to maximize both shared code and implementation robustness. #### Composition Over Inheritance Rellm's APIs are designed using composition over inheritance. For instance, an [`Event`](#rellm-Event) contains a [`Post`](#rellm-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. #### Predictable Atomicity The use of composition over inheritance also means that Rellm APIs can be *predictably* non-atomic based on their compositional structure. For instance, [`UpdatePost`](#grpc-api-UpdatePost) is fully atomic. [`UpdateEvent`](#grpc-api-UpdateEvent), however, is non-atomic. Given that an [`Event`](#rellm-Event) has a [`Post`](#rellm-Post) and many [`Occasion`](#rellm-Occasion)s, [`UpdateEvent`](#grpc-api-UpdateEvent) is implemented as a composition of four other RPCs - each independently callable and individually atomic -- run in a fixed order: [`UpdateEventDetails`](#grpc-api-UpdateEventDetails) (which itself first updates the [`Event`](#rellm-Event)'s own [`Post`](#rellm-Post) atomically, literally calling the [`UpdatePost`](#grpc-api-UpdatePost) RPC), then [`CreateNewOccasions`](#grpc-api-CreateNewOccasions), [`UpdateOccasions`](#grpc-api-UpdateOccasions), and finally [`DeleteRemovedOccasions`](#grpc-api-DeleteRemovedOccasions). Create must run before Delete so that a request which both drops an old [`Occasion`](#rellm-Occasion) and adds a new one never transiently leaves the [`Event`](#rellm-Event) with zero Occasions. Because moderation/visibility lives at the [`Post`](#rellm-Post) level, and [`UpdateEventDetails`](#grpc-api-UpdateEventDetails) runs first, this means that a developer error in the later [`Occasion`](#rellm-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. ### Core Types Rellm's data model centers around a handful of top-level types, most of which carry their own [`Visibility`](#rellm-Visibility) and [`Moderation`](#rellm-Moderation) state and can be organized into [`Group`](#rellm-Group)s. #### ServerConfiguration Rellm incorporates server configuration, including fairly deep customization of the end-user UI/UX, as perhaps its *most* primitive type. [`ServerConfiguration`](#rellm-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`](#rellm-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`](#rellm-ServerConfiguration), then worry about auth, then finally about retrieving anything else." ##### Server Info and Theme [`ServerInfo`](#rellm-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`](#rellm-ServerLogo) (separate light/dark, square/wide media IDs), a [`ServerColors`](#rellm-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). ##### Custom Tabs [`CustomNavigationTabSet`](#rellm-CustomNavigationTabSet) (`custom_tabs`) lets a server admin override the Elm UI's default navigation. `home` (a [`CustomHomePage`](#rellm-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`](#rellm-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`](#rellm-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. ##### Anonymous, Default, and Basic User Permission Sets Three [`Permission`](#rellm-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`. ##### Federation Settings [`FederationInfo`](#rellm-FederationInfo) (`federation_info`) is where all federation and social-sync credentials live. ###### Other Rellm servers `servers` (repeated [`FederatedServer`](#rellm-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/ActivityPub servers `mastodon_servers` (repeated [`MastodonServer`](#rellm-MastodonServer)) plays a similar role to `servers` above, but for Mastodon instances instead of other Rellm servers - see [Cross-Protocol Federation](#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`](#rellm-Post) shape, but it has no equivalent of Rellm's Events, Groups, Media library, or People/Follows - just posts and their authors. ###### Facebook API Keys `facebook_auth_config` (a [`FacebookAuthConfig`](#rellm-FacebookAuthConfig), `app_id`/`app_secret`) registers this server's Facebook App, enabling users to connect Facebook Page and Instagram Business [`SyncDestination`](#rellm-SyncDestination)s. `app_secret` is write-only/never serialized back to clients; admins set/rotate it via [`ConfigureServer`](#grpc-api-ConfigureServer) (i.e. the same admin UI form that manages the rest of [`ServerConfiguration`](#rellm-ServerConfiguration)) - the secret is simply never echoed back in subsequent [`GetServerConfiguration`](#grpc-api-GetServerConfiguration) responses. ###### X (Twitter) API Keys `x_twitter_auth_config` (an [`XTwitterAuthConfig`](#rellm-XTwitterAuthConfig), `client_id`/`client_secret`) registers this server's X Developer App, enabling users to connect X [`SyncDestination`](#rellm-SyncDestination)s - until set, X SyncDestinations fail with `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. ##### Web Push Configuration [`WebPushConfig`](#rellm-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`](#grpc-api-ConfigureServer), not by editing the database directly. ##### CDN Configuration [`ExternalCDNConfig`](#rellm-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. #### User A [`User`](#rellm-User) is a Rellm account: username, real name, bio, avatar, contact methods, and [`Permission`](#rellm-Permission)s, plus counts (followers, posts, events, etc.) and federation info (see [Federated Profiles](#federated-profiles) above). A lighter-weight [`Author`](#rellm-Author) (just ID, username, avatar, real name, permissions) is embedded on [`Post`](#rellm-Post)s, [`Message`](#rellm-Message)s, and similar content types instead of a full [`User`](#rellm-User), to keep those payloads small. ##### Follows A [`Follow`](#rellm-Follow) is one [`User`](#rellm-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. ##### Memberships A [`Membership`](#rellm-Membership) is a [`User`](#rellm-User)'s membership (or pending join request/invitation) in a [`Group`](#rellm-Group), tracking the user's [`Permission`](#rellm-Permission)s within the group plus separate group-side and user-side [`Moderation`](#rellm-Moderation) (for join-approval flows). Returned as part of [`User`](#rellm-User)/[`Group`](#rellm-Group) payloads, and via [`Member`](#rellm-Member) when listing a Group's members. ##### SyncSources While Federation is a first-class feature of Rellm, a [`User`](#rellm-User) can also own many [`SyncSource`](#rellm-SyncSource)s - server-owned external origins to sync [`Post`](#rellm-Post)s 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 [`Event`](#rellm-Event)s/ [`Occasion`](#rellm-Occasion)s), or an RSS/Atom subscription URL (`configuration.rss_subscription_url`/ `configuration.atom_subscription_url`, syncing in plain [`Post`](#rellm-Post)s). Every kind of synced content is tagged via its own `Post.sync_source` - an [`Event`](#rellm-Event)'s own Post, each of its [`Occasion`](#rellm-Occasion)s' 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`](#grpc-api-GetSyncSources), [`CreateSyncSource`](#grpc-api-CreateSyncSource) (requires `SYNC_EVENTS_FROM_ICS`/`SYNC_POSTS_FROM_RSS`/`SYNC_POSTS_FROM_ATOM` - whichever matches the source's own configuration - or Admin), [`UpdateSyncSource`](#grpc-api-UpdateSyncSource), and [`DeleteSyncSource`](#grpc-api-DeleteSyncSource). See also: [`SyncDestination`](#rellm-SyncDestination) ###### iCal `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`](#rellm-Event) (and one [`Occasion`](#rellm-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. ###### RSS `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`](#rellm-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. ###### Atom `configuration.atom_subscription_url` is a plain Atom subscription URL, behaving identically to RSS (above) -- one plain [`Post`](#rellm-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. ##### SyncDestinations A [`User`](#rellm-User) can also own many [`SyncDestination`](#rellm-SyncDestination)s - user-owned external targets to push [`Occasion`](#rellm-Occasion)s and [`Post`](#rellm-Post)s 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`](#rellm-Occasion) or [`Post`](#rellm-Post) (not, say, the parent [`Event`](#rellm-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`](#rellm-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`](#rellm-ServerConfiguration)'s `federation_info`. Destinations are managed via [`GetSyncDestinations`](#grpc-api-GetSyncDestinations), [`CreateSyncDestination`](#grpc-api-CreateSyncDestination), [`UpdateSyncDestination`](#grpc-api-UpdateSyncDestination), and [`DeleteSyncDestination`](#grpc-api-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`](#rellm-Occasion) or [`Post`](#rellm-Post) to a destination is a separate step, via [`SyncOccasion`](#grpc-api-SyncOccasion)/ [`DeleteOccasionSyncDestination`](#grpc-api-DeleteOccasionSyncDestination) and [`SyncPost`](#grpc-api-SyncPost)/[`DeletePostSyncDestination`](#grpc-api-DeletePostSyncDestination), gated the same way (the `_EVENTS_`/`_POSTS_` half matching which RPC). See also: [`SyncSource`](#rellm-SyncSource) ###### Facebook `configuration.facebook_page` (a [`FacebookPage`](#rellm-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`. ###### Instagram `configuration.instagram_account` (an [`InstagramAccount`](#rellm-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`](#rellm-Post)/[`Occasion`](#rellm-Occasion) with no attached media fails with `instagram_requires_media`. Gated on `SYNC_EVENTS_TO_INSTAGRAM`/`SYNC_POSTS_TO_INSTAGRAM`. ###### Mastodon `configuration.mastodon_account` (a [`MastodonAccount`](#rellm-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`. ###### Bluesky `configuration.bluesky_account` (a [`BlueskyAccount`](#rellm-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`. ###### X (Twitter) `configuration.x_twitter_account` (an [`XTwitterAccount`](#rellm-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`](#rellm-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`. ###### Threads `configuration.threads_account` (a [`ThreadsAccount`](#rellm-ThreadsAccount)) is a connected Threads account. Threads API is a product added to this server's *existing* Facebook App (see [`FacebookAuthConfig`](#rellm-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`. ##### AIProviders A [`User`](#rellm-User) can also own many [`AIProvider`](#rellm-AIProvider)s - connections to external AI model APIs (e.g. a Gemini or OpenAI API key) - and grant other users metered access to them via [`AIProviderGrant`](#rellm-AIProviderGrant)s. See `ai_providers.proto` and the AIProvider section below. Which models are actually available, and what each can do ([`AIModelCapability`](#rellm-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`](https://github.com/JonLatane/rellm/blob/main/backend/src/logic/ai_model_catalog.rs) on GitHub for the actual source of truth. #### AIProvider An [`AIProvider`](#rellm-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`](#rellm-SyncDestination)/[`SyncSource`](#rellm-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`](#rellm-SyncDestination)'s platform credentials, the actual API key is write-only - accepted on [`CreateAIProvider`](#grpc-api-CreateAIProvider)/[`UpdateAIProvider`](#grpc-api-UpdateAIProvider) but never populated back in a response. Providers are managed via [`GetAIProviders`](#grpc-api-GetAIProviders), [`CreateAIProvider`](#grpc-api-CreateAIProvider) (requires `CREATE_AI_PROVIDERS`, or Admin), [`UpdateAIProvider`](#grpc-api-UpdateAIProvider), and [`DeleteAIProvider`](#grpc-api-DeleteAIProvider) - each gated self-or-Admin, the same shape as [`SyncDestination`](#rellm-SyncDestination)'s RPCs. ##### Gemini `provider.gemini_credentials` (a [`GeminiCredentials`](#rellm-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. ##### OpenAI `provider.openai_credentials` (an [`OpenAICredentials`](#rellm-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). ##### Anthropic `provider.anthropic_credentials` (an [`AnthropicCredentials`](#rellm-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. ##### DigitalOcean `provider.digitalocean_credentials` (a [`DigitalOceanCredentials`](#rellm-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). ##### AIProviderGrants A provider's owner may share metered access to it with other users via [`AIProviderGrant`](#rellm-AIProviderGrant)s, each carrying a `tokens_remaining` budget for that grantee. Granted/reset via [`GrantAIProvider`](#grpc-api-GrantAIProvider) (upserted on the unique `(ai_provider_id, grantee)` pair - granting again *resets*, rather than adds to, `tokens_remaining`) and removed via [`RevokeAIProvider`](#grpc-api-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 Rellm's Market (`market.proto`) is this server's storefront - a small, Stripe-backed marketplace an admin stocks with up to twelve [`MarketProduct`](#rellm-MarketProduct)s (one offering type times one billing period each) that any user can buy. Four offering types exist today ([`PurchaseType`](#rellm-PurchaseType)): `PURCHASE_TYPE_MEDIA_STORAGE` (raises the buyer's `User.media_storage_limit_bytes`), `PURCHASE_TYPE_AI_GRANTS` (grants/resets an [`AIProviderGrant`](#rellm-AIProviderGrant) against one of the server operator's [`AIProvider`](#rellm-AIProvider)s), `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`](#grpc-api-UpdateMarketSubscription), tracks these orders - [`RellmHostingSubscriptionDetails`](#rellm-RellmHostingSubscriptionDetails)'s own `fulfillment_status`/ `fulfillment_notes` fields), and `PURCHASE_TYPE_PERMISSIONS_ACCESS` (grants the buyer a fixed set of `Permission`s, e.g. pay-gating `SYNC_EVENTS_TO_FACEBOOK`). Every purchase gets a [`MarketSubscription`](#rellm-MarketSubscription) -- even a one-time ([`PurchasePeriod`](#rellm-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`](#rellm-MarketPurchase) per renewal; an indefinite one's stays at exactly one. Products are listed via [`GetMarketProducts`](#grpc-api-GetMarketProducts) (unauthenticated; admins additionally see delisted ones) and managed via [`CreateMarketProduct`](#grpc-api-CreateMarketProduct)/[`UpdateMarketProduct`](#grpc-api-UpdateMarketProduct) (both Admin-only; a [`MarketProduct`](#rellm-MarketProduct)'s `type`/`period` are immutable once created). Buying one is a two-step, webhook-settled flow: [`MakeMarketPurchase`](#grpc-api-MakeMarketPurchase) starts a Stripe Checkout Session and returns its URL to redirect the buyer to - no [`MarketPurchase`](#rellm-MarketPurchase)/[`MarketSubscription`](#rellm-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`](#rellm-MarketPurchase)/[`MarketSubscription`](#rellm-MarketSubscription) and apply its entitlement. A recurring [`MarketSubscription`](#rellm-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`](#grpc-api-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_user_media_allocation_bytes`, or removes the granted `Permission`s, depending on `type`) and sets `service_terminated_at`. A user's own MarketSubscriptions (never anyone else's) are listed via [`GetMarketSubscriptions`](#grpc-api-GetMarketSubscriptions), and also travel along on [`User`](#rellm-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`](#rellm-StripeConfig)), Admin-only like `twilio_config`/`bird_config`. #### Media [`Media`](#rellm-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`](#rellm-MediaReference) type. #### Post [`Post`](#rellm-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`](#rellm-PostContext)) as the backing data for replies, [`Event`](#rellm-Event)s, and [`Occasion`](#rellm-Occasion)s alike. Posts can be replied to (threaded via `reply_to_post_id`), cross-posted to [`Group`](#rellm-Group)s ([`GroupPost`](#rellm-GroupPost)), and shared directly with users ([`UserPost`](#rellm-UserPost)). ##### GroupPosts A [`GroupPost`](#rellm-GroupPost) is the cross-posting of a [`Post`](#rellm-Post) into a [`Group`](#rellm-Group), carrying the group-specific moderation status and who shared it, separately from the Post's own (author-set) visibility/moderation. ##### UserPosts A [`UserPost`](#rellm-UserPost) is a "direct share" of a [`Post`](#rellm-Post) to a [`User`](#rellm-User) (see also `DIRECT` [`Visibility`](#rellm-Visibility)). Currently unused/unimplemented. ##### SyncDestinations A [`Post`](#rellm-Post) may also be synced (cross-posted) out to a user-owned [`SyncDestination`](#rellm-SyncDestination) (e.g. a connected Facebook Page), the same mechanism [`Occasion`](#rellm-Occasion)s use (see below) - each Post may push to several destinations at once, tracked via the repeated `Post.sync_destinations` (each a [`SyncDestinationStatus`](#rellm-SyncDestinationStatus)). #### Event An [`Event`](#rellm-Event) is a wrapper for *at least two* [`Post`](#rellm-Post)s. It always has its own top-level [`Post`](#rellm-Post) (`PostContext.EVENT`, holding the event's overall title/description) *and* it must have at least one [`Occasion`](#rellm-Occasion) (see below), each of which in turn must have its own [`Post`](#rellm-Post) (`PostContext.OCCASION`, carrying that Occasion's start/end time, [`Location`](#rellm-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. ##### Occasions An [`Occasion`](#rellm-Occasion) is the actual time-boxed occurrence of an [`Event`](#rellm-Event) - it carries the `starts_at`/`ends_at` timestamps and optional [`Location`](#rellm-Location) that the parent [`Event`](#rellm-Event) itself does not have. An [`Event`](#rellm-Event) with zero Occasions is meaningless (no time or place to attach to), so every [`Event`](#rellm-Event) must have at least one. - **Rsvps**: An [`Rsvp`](#rellm-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. #### Group A [`Group`](#rellm-Group) organizes [`User`](#rellm-User)s, [`Post`](#rellm-Post)s, and [`Event`](#rellm-Event)s together under shared visibility, moderation, and permission defaults. ##### Memberships A [`Membership`](#rellm-Membership) is a [`User`](#rellm-User)'s membership (or pending join request/invitation) in a [`Group`](#rellm-Group), tracking the user's [`Permission`](#rellm-Permission)s within the group plus separate group-side and user-side [`Moderation`](#rellm-Moderation) (for join-approval flows). Returned as part of [`User`](#rellm-User)/[`Group`](#rellm-Group) payloads, and via [`Member`](#rellm-Member) when listing a Group's members. ##### GroupPosts A [`GroupPost`](#rellm-GroupPost) is the cross-posting of a [`Post`](#rellm-Post) into a [`Group`](#rellm-Group), carrying the group-specific moderation status and who shared it, separately from the Post's own (author-set) visibility/moderation. #### Message [`Message`](#rellm-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](#post-email-stalwart-email-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. ##### MessagingGroup A [`MessagingGroup`](#rellm-MessagingGroup) is the set of participants in a Message conversation. Every [`Message`](#rellm-Message) belongs to one; if a client wasn't a visible recipient (e.g. they were BCC'ed), the [`Message`](#rellm-Message) they receive omits it. ### Authentication Rellm uses a standard OAuth2 flow (over gRPC) for authentication, with rotating `access_token`s and `refresh_token`s (both [`ExpirableToken`s](#rellm-ExpirableToken)). 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](#http-based-client-host-negotiation-for-external-cdns-get-backend_host), and check its [`GetServiceVersion`](#grpc-api-GetServiceVersion) and [`GetServerConfiguration`](#grpc-api-GetServerConfiguration) RPCs. Check whether you have the `CREATE_ACCOUNT` and/or `LOGIN` [`AuthenticationFeature`](#rellm-AuthenticationFeature)s in your [`ServerConfiguration`](#rellm-ServerConfiguration). Next, use the [`CreateAccount`](#grpc-api-CreateAccount) or [`Login`](#grpc-api-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`](#grpc-api-AccessToken) RPC for a new one. (The [`AccessToken`](#grpc-api-AccessToken) RPC may, at random, also return a new `refresh_token`. If so, it should immediately replace the old one in client storage.) #### Federated Authentication tl;dr: Lets you sign in to the `jon@bullcity.social` 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/...`](#authtopublic_keyrequesting_host-sending-side) and [`/auth/from/...`](#authfromencrypted_account_auth_tokens-receiving-side)) exchange an encrypted pair of tokens via a full-page redirect; no gRPC/HTTP endpoint on either backend is involved beyond the [`Login`](#grpc-api-Login) RPC itself (plus [`GetCurrentUser`](#grpc-api-GetCurrentUser) on the receiving side, to hydrate everything else - see step 6). 1. Say a user is on `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. 2. Clicking it does a full-page navigation to `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`. 3. `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. 4. The user authenticates via the [`Login`](#grpc-api-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`](#grpc-api-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.") 5. Only `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}`. 6. `jonline.io` decrypts the payload with the private key it generated in step 2, calls [`GetCurrentUser`](#grpc-api-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](#authtopublic_keyrequesting_host-and-authfromencrypted_account_auth_tokens-receiving-side) page routes below for the exact URL/crypto shape. ##### Developer option: signing in to an unsecure `localhost` For local development, a frontend running at plaintext `http://localhost[:port]` can be the *requesting* side of the flow above (e.g. signing in to your local dev server using an account from a production server). Since the encrypted tokens are then handed to a non-TLS origin, this is **off by default** and opt-in per *sending* server: an admin turns on `FederationInfo.unsecure_localhost_federated_auth_enabled` (checkbox on the Elm frontend's Server Information > Federation tab, saved on toggle). Without it: * [`/auth/to`](#authtopublic_keyrequesting_host-sending-side) refuses to log in or redirect when `{requesting_host}` is `localhost`, disabling its submit button and showing an error; * [`/auth/from`](#authfromencrypted_account_auth_tokens-receiving-side), when loaded over plain `http://`, will not call [`GetCurrentUser`](#grpc-api-GetCurrentUser) with tokens from a server that hasn't opted in. With it enabled, `/auth/to` redirects to `http://localhost/...` (rather than `https://`) and shows a warning that the sign-in is going to an unsecure localhost; only `localhost` is ever allowed over `http://`. This is purely a frontend-enforced guard -- the backend can't stop a modified client from moving its own tokens anywhere -- so only enable it on servers used for development. ### Federation 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. #### Federated Servers Rellm servers can recommend other servers to clients with the `federation_info` field (a [`FederationInfo` message](#rellm-FederationInfo)) in [`ServerConfiguration`](#rellm-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. #### Federated Profiles 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`](https://jonline.io/jon), [`oakcity.social/jon`](https://oakcity.social/jon), and [`bullcity.social/jon`](https://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`](https://bullcity.social/jon) and [`bullcity.social/openmic`](https://bullcity.social/openmic) are linked together, but [`bullcity.social/openmic`](https://bullcity.social/openmic) isn't linked to [`jonline.io/jon`](https://jonline.io/jon) or [`oakcity.social/jon`](https://oakcity.social/jon). Federated profiles are managed via the `federated_profiles` field (a `repeated` [`FederatedAccount`](#rellm-FederatedAccount)) in the [`User`](#rellm-User) message. #### Federated Browsing 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. #### Federated Messaging 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.) #### Federated Markets 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 [`MarketProduct`](#rellm-MarketProduct)s 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`](#grpc-api-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. ### HTTP Endpoints #### Internal HTTP server (27705) ##### `POST /email`: Stalwart Email Integration Delivery endpoint called by the [Stalwart](https://stalw.art) mail server (see [`deploys/email`](https://github.com/JonLatane/rellm/tree/main/deploys/email)'s [README](https://github.com/JonLatane/rellm/blob/main/deploys/email/README.md) for setup/architecture) once it accepts an inbound message addressed to one of this Rellm instance's onboarded domains, turning it into a [`Message`](#rellm-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). * **Request**: the body is Stalwart's `data`-stage [MTA Hook](https://stalw.art/docs/mta/filter/mtahooks/) 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: ```json { "envelope": { "to": [{ "address": "someone@yourdomain.com" }] }, "message": { "headers": [["Subject", "Hello"], ["From", "sender@example.com"], ["To", "someone@yourdomain.com"]], "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`. * **Recipient resolution**: each envelope address's local part (before the `@`) 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`. * **Storage**: matched recipients become a [`Message`](#rellm-Message) addressed to a [`MessagingGroup`](#rellm-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`](#rellm-Message). The [`Message`](#rellm-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 object storage used for [`Media`](#rellm-Media). Duplicate deliveries of the same `Message-ID` (Stalwart retries on transient failure) reuse the existing [`Message`](#rellm-Message) row rather than storing/uploading a duplicate. * **Response**: `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. #### External HTTP servers (80, 8000, 443) 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](https://github.com/JonLatane/rellm/blob/main/frontends/tamagui/packages/app/store/clients.ts#L116) and [Flutter](https://github.com/JonLatane/rellm/blob/main/frontends/flutter/lib/models/rellm_clients.dart#L26) 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`](#rellm-Post) from an unauthenticated [`GetPosts`](#grpc-api-GetPosts) (the same "first page" an anonymous visitor sees) as `/post/{id}`, and every [`Occasion`](#rellm-Occasion) from an unauthenticated [`GetEvents`](#grpc-api-GetEvents) starting `EventSettings.calendar_lookback_days` (or 14, if unset) ago as `/event/{occasion_id}`. It does not (yet) enumerate individual [`User`](#rellm-User) pages. ##### `GET /favicon.ico`: ICO Favicon Serves the server's configured logo (`ServerConfiguration.server_info.logo.square_media_id`, a [`Media`](#rellm-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 Favicon As `GET /favicon.ico` above, but serves (and if necessary converts to) `.png` instead. ##### `POST /media`: Upload Media See the [Media](#rellm-Media) section for the [`Media`](#rellm-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`](#rellm-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 - **moderation/visibility/permission checks on read are not yet enforced** (a `TODO` in `media_file`'s implementation), so a [`Media`](#rellm-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 /calendar.ics?post_id={id}`: Event or Occasion Calendar Serves a calendar for a single [`Event`](#rellm-Event) -- e.g. for an "Add to Calendar" download. If `post_id` is the Event's own Post ID, every one of its [`Occasion`](#rellm-Occasion)s is included; if it is an Occasion's Post ID, only that Occasion is. `post_id` takes precedence over `user_id`, and the calendar is named after the Event's title. Each VEVENT has a stable `UID` of `{occasion_post_id}@{frontend_domain}`, so re-importing updates rather than duplicates. In the Elm frontend, this powers the event/occasion "Add to Calendar" export. ##### `GET /calendar.ics?anonymous_auth_token={token}`: Anonymous RSVP Calendar Optional on any of the `/calendar.ics` forms above. Passed straight through to `GetEventsRequest.anonymous_attendee_auth_token`, so it accepts the same forms: a plain `<token>`, or several as `<occasionId>-<token>--<occasionId>-<token>...` (as in the web frontends' `?anonymousAuthToken=` parameter). It reveals the real location of Occasions whose location is hidden until the viewer's RSVP is approved (`EventInfo.hide_location_until_rsvp_approved`), and adds a "manage your RSVP" link (`/event/{occasion_id}?anonymousAuthToken={token}`) to the description of each Occasion the token holds an RSVP for. Treat such URLs as secrets, since the token alone grants control of that RSVP. ##### `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 [`Post`](#rellm-Post)s 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. ### Web UI paths 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](https://github.com/JonLatane/rellm/tree/main/frontends/elm-spa/src/Pages) is (`{name}` denotes a dynamic path segment; `[@{host}]` marks where a [federated](#federated-profiles) `{username}@{host}`-style suffix is accepted for that segment): #### `/`: Home The community's latest activity. #### `/posts`: Posts The Posts listing. ##### `/post/{postId}[@{host}]`: Post An individual [`Post`](#rellm-Post) - including [`Event`](#rellm-Event)/[`Occasion`](#rellm-Occasion) posts and replies, which are [`Post`](#rellm-Post)s themselves (see [Post](#post) above). #### `/events`: Events The Events listing. #### `/[-._~:/?[]@!$&'()*+,;%=]{postId}`: Short Post/Event URLs A [`Post`](#rellm-Post) or [`Event`](#rellm-Event)/[`Occasion`](#rellm-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`](https://github.com/JonLatane/rellm/blob/main/backend/src/rpcs/validations/validate_fields.rs)'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}]`: Event An individual [`Event`](#rellm-Event), looked up by its own `post.id` or any of its [`Occasion`](#rellm-Occasion)s' `post.id`s. ##### `/event_ai`: AI Event Importer Tamagui-only, for now - an AI-assisted bulk [`Event`](#rellm-Event) importer. Elm doesn't have this page yet. #### `/people`: People The People listing. ##### `/people/follow_requests`: Follow Requests The current user's pending [`Follow`](#rellm-Follow) requests. ##### `/user/{userId}`: Profile A [`User`](#rellm-User) profile looked up by (stable) user ID. #### `/{custom_tab_or_username}`: User pages by username, or a custom tab The same [`User`](#rellm-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`](#rellm-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}`: Groups A [`Group`](#rellm-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}]`: Post An individual [`Post`](#rellm-Post) cross-posted into the group. ##### `/g/{shortname}/events`: Events ##### `/g/{shortname}/e/{occasionId}[@{host}]`: Event ##### `/g/{shortname}/members`: Members ##### `/g/{shortname}/m/{username}`: Member An individual [`Member`](#rellm-Member)'s details. #### `/server/{serverIdentifier}`: Server Information about a (possibly federated) Rellm server. #### `/about`, `/about_rellm`: About This 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](#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. * **Path params**: `{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`](#authfromencrypted_account_auth_tokens-receiving-side) page below expects, so the split is unambiguous). * **Query params**: `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`](#authfromencrypted_account_auth_tokens-receiving-side) redirect. * **Behavior**: shows a sign-in form for *this* server (or a "currently signed in as ..." badge, if already authenticated here), plus a "Sign back in here"/"Also sign in here" checkbox (checked by default). Submitting calls the [`Login`](#grpc-api-Login) RPC (always a fresh login, never reusing stored tokens) to mint the transfer tokens; if the checkbox is checked, a second independent [`Login`](#grpc-api-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`](#authtopublic_keyrequesting_host-sending-side) above. Reached only via that redirect. * **Path params**: `{encrypted_account_auth_tokens}` is the `ephemeral_public_key.iv.ciphertext` blob produced by [`/auth/to`](#authtopublic_keyrequesting_host-sending-side). * **Query params**: `start_path`, passed through unchanged from [`/auth/to`](#authtopublic_keyrequesting_host-sending-side); defaults to `/` if missing. * **Behavior**: decrypts `{encrypted_account_auth_tokens}` using the private key this origin generated when it built the [`/auth/to`](#authtopublic_keyrequesting_host-sending-side) link (same ECDH + HKDF-SHA256 + AES-GCM derivation, in reverse), yielding `bullcity.social`'s hostname and its `refresh_token`/`access_token`. Calls [`GetCurrentUser`](#grpc-api-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. ### gRPC API
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.*
Request for a new access token using a refresh token.
The refresh token to use to request a new access token.
Optional *requested* expiration time for the token. Server may ignore this.
Returned when requesting access tokens.
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
The new access token.
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.
Sets the server's [`CustomCSSConfiguration`](#rellm-CustomCSSConfiguration), creating a new server configuration version (everything else is copied from the current one). Validated: at most 32 `media_ids`, each an existing `GLOBAL_PUBLIC` Media, at most 64 KiB of `custom_css`, and not both `force_light_theme` and `force_dark_theme`. An unset `custom_css` keeps the stored stylesheet. *Authenticated.* Requires `ADMIN` permissions.
Configure the server (i.e. the response to [`GetServerConfiguration`](#grpc-api-GetServerConfiguration)). *Authenticated.* Requires `ADMIN` permissions. Editing `cluster_resources` additionally requires `EDIT_CLUSTER_SETTINGS` - see that field's own doc. Editing [`supported_contact_protocols`](#rellm-ServerConfiguration) is validated, not just stored -- see [`ContactProtocol`](#rellm-ContactProtocol)'s own doc.
Creates an AIProvider for the current user. *Authenticated*, requires `CREATE_AI_PROVIDERS` (or Admin).
Creates a user account and provides a `refresh_token` (along with an `access_token`). *Publicly accessible.*
Request to create a new account.
Username for the account to be created. Must not exist.
Password for the account to be created. Must be at least 8 characters.
Email to be used as a contact method.
Phone number to be used as a contact method.
Request an expiration time for the Auth Token returned. By default it will not expire.
(Not yet implemented.) The name of the device being used to create the account.
Creates an Event. *Authenticated.*
Follow (or request to follow) a user. *Authenticated.*
Creates a group with the current user as its admin. *Authenticated.* Requires the `CREATE_GROUPS` permission.
Cross-post a Post to a Group. *Authenticated.*
Creates a MarketProduct. *Authenticated*, requires Admin.
Requests to join a group (or joins it), or sends an invite to the user. *Authenticated.* Memberships and moderations are set to their defaults.
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.
Creates a Post. *Authenticated.*
Creates a SyncDestination for the current user. *Authenticated*, requires `SYNC_EVENTS_TO_FACEBOOK` or `SYNC_POSTS_TO_FACEBOOK` (or Admin).
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).
*Authenticated*.
Deletes an AIProvider (and its AIProviderGrants). *Authenticated* (owner, or Admin).
Request to delete an AIProvider. Also deletes any of its [`AIProviderGrant`](#rellm-AIProviderGrant)s.
The provider to be deleted.
(Soft) deletes a Event. Returns the deleted version of the Event. *Authenticated.*
Unfollow (or unrequest) a user. *Authenticated.*
Delete a Group. *Authenticated.* Requires `ADMIN` permissions within the group, or `ADMIN` permissions for the user.
Delete a GroupPost. *Authenticated.*
Unlinks all generated link preview images (see [`MediaSettings.disable_link_preview_images`](#rellm-MediaSettings)) from their posts and makes every post with a link eligible for preview generation again -- including posts that had given up after repeated failures. The old preview media is orphaned, not deleted: it's removed from storage by the next [`DeleteUnownedMedia`](#grpc-api-DeleteUnownedMedia) run (the periodic `delete_unowned_media` job, or call it directly). *Authenticated.* Requires `ADMIN` permissions.
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/object storage. Deleting other users' media requires `ADMIN` permissions.
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.
Leave a group (or cancel membership request). *Authenticated.*
Removes an Occasion's sync (cross-post) to a SyncDestination, the reverse of [`SyncOccasion`](#grpc-api-SyncOccasion). *Authenticated* (destination owner, or Admin), requires `SYNC_EVENTS_TO_FACEBOOK` (or Admin).
Removes a single Occasion's sync (cross-post) to one SyncDestination - the reverse of [`SyncOccasion`](#grpc-api-SyncOccasion). Does not delete the post already made on the destination (e.g. the Facebook Page post), only the local sync record.
The Occasion to un-sync.
The SyncDestination to un-sync it from.
(TODO) (Soft) deletes a Post. Returns the deleted version of the Post. *Authenticated.*
Removes a Post's sync (cross-post) to a SyncDestination, the reverse of [`SyncPost`](#grpc-api-SyncPost). *Authenticated* (destination owner, or Admin), requires `SYNC_POSTS_TO_FACEBOOK` (or Admin).
Removes a single Post's sync (cross-post) to one SyncDestination - the reverse of [`SyncPost`](#grpc-api-SyncPost). Does not delete the post already made on the destination (e.g. the Facebook Page post), only the local sync record.
The Post to un-sync.
The SyncDestination to un-sync it from.
Deletes Occasions in an existing Event that aren't present in the input Event. *Authenticated.*
Delete an Rsvp. *Publicly accessible **or** Authenticated, with anonymous RSVP support.*
Deletes a SyncDestination. *Authenticated* (owner, or Admin).
Request to delete a SyncDestination.
The destination to be deleted.
Whether to also delete posts already made on the destination (e.g. the Facebook Page posts).
Deletes a SyncSource. *Authenticated* (owner, or Admin).
Request to delete a SyncSource.
The source to be deleted.
Whether to delete synced events.
Deletes all Media with no owner (e.g. orphaned by [`DeleteLinkPreviewImages`](#grpc-api-DeleteLinkPreviewImages) or by deleted users) from object storage and the database, and removes it from any posts still referencing it. *Authenticated.* Requires `ADMIN` permissions.
Deletes a user by ID. *Authenticated.* Deleting other users requires `ADMIN` permissions.
Federate the current user's profile with another user profile. *Authenticated*.
Releases resources previously acquired via [`LockClusterResources`](#grpc-api-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`](#rellm-FreeClusterResourcesRequest)'s own doc for its no-op-if-not-held behavior.
Releases resources this `namespace_id` previously locked via [`LockClusterResources`](#grpc-api-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.
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.
Which resources to release.
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.
Request to generate (or edit) an image via one of the current user's [`AIModel`](#rellm-AIModel)s - see [`GenerateMedia`](#grpc-api-GenerateMedia). The resulting image is stored as a new [`Media`](#rellm-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.
Which of the current user's `AIModel`s 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.
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 [`SyncDestination`](#rellm-SyncDestination)s use) before being sent to the model, so the user never has to paste that context in by hand.
Existing [`Media`](#rellm-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`](#rellm-AIModelCapability)'s own doc). Every id must be owned by the current user (or the current user must be an Admin).
What, if anything, the generated image should be attached to (as the *first* item in its `media` list) once generated, and whose content gets folded into `user_prompt` as context - see this message's own doc. Leave unset to just generate/store the image in the current user's own Media (as `MyMediaPanel` shows), without attaching it to anything.
Attach to (and use the content of) this Post. Caller must be its author, or an Admin.
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`](#grpc-api-SyncOccasion) does). Caller must be the Event's own Post's author, or hold `MODERATE_POSTS`/`MODERATE_EVENTS`, or be an Admin.
Gets a user's AIProviders. *Authenticated* (self, or Admin for any user).
Response to a request for a user's [`AIProvider`](#rellm-AIProvider)s.
The requested user's own AIProviders (those they own) - exactly the distinct `provider`s 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.
Every model the requested user may currently call - their own providers' models, plus any models granted to them on other users' providers. See [`AIModel`](#rellm-AIModel)'s own doc.
Gets the current user. *Authenticated.*
Gets the server's [`CustomCSSConfiguration`](#rellm-CustomCSSConfiguration), including the (potentially large) `custom_css` text -- the only RPC that returns it: [`GetServerConfiguration`](#grpc-api-GetServerConfiguration) carries the rest of the message (media, forced theme) but never loads the stylesheet. An unset configuration returns an empty one. *Publicly accessible.*
Gets Events. *Publicly accessible **or** Authenticated.* Unauthenticated calls only return Events of `GLOBAL_PUBLIC` visibility.
Request to get Events in a formatted *per-Occasion* structure. i.e. the response will carry duplicate [`Event`](#rellm-Event)s with the same ID if that [`Event`](#rellm-Event) has multiple [`Occasion`](#rellm-Occasion)s 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)
Limits results to those by the given author user ID.
Excludes events by any of the given author user IDs. Applies to every feed-style listing (all but single `post_id`/`occasion_post_ids` lookups). Numbered out of order (it logically belongs right after `author_user_id`) so as not to break compatibility; the order will be straightened out in a later protocol version.
Limits results to those in the given group ID (via [`GroupPost`](#rellm-GroupPost) association's for the Event's internal [`Post`](#rellm-Post)).
Filters returned [`Occasion`](#rellm-Occasion)s by time.
If set, only returns events that the given user is attending. If `rsvp_statuses` is also set, returns events where that user's status is one of the given statuses.
If set, only return events for which the current user's RSVP 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.
Finds Events for the Post with the given ID. The Post should have a [`PostContext`](#rellm-PostContext) of `EVENT` or `OCCASION`.
The listing type, e.g. `ALL_ACCESSIBLE_EVENTS`, `FOLLOWING_EVENTS`, `MY_GROUPS_EVENTS`, `DIRECT_EVENTS`, `GROUP_EVENTS`, `GROUP_EVENTS_PENDING_MODERATION`.
Search text for full-text search.
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.
Auth token(s) proving ownership of anonymous RSVPs, mirroring `GetRsvpsRequest.anonymous_attendee_auth_token`. Either a plain `<token>`, or several at once as `<occasionId>-<token>--<occasionId>-<token>...` (the form the web frontends keep in their `?anonymousAuthToken=` URL parameter, so it can be passed straight through) -- the occasion id prefix is only advisory; every token is tried against every returned occasion's RSVPs. Lets an anonymous attendee's own (possibly still-`PENDING`) [`Rsvp`](#rellm-Rsvp) and its `Occasion.location` (when `EventInfo.hide_location_until_rsvp_approved` is set) surface via each returned `Occasion.rsvps`/`current_user_rsvp`, same as a logged-in user's own RSVP does automatically.
A list of [`Event`](#rellm-Event)s with a maybe-incomplete (see [`GetEventsRequest`](#rellm-GetEventsRequest)) set of their [`Occasion`](#rellm-Occasion)s. 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.)
Get GroupPosts for a Post (and optional group). *Publicly accessible **or** Authenticated.*
Used for getting context about [`GroupPost`](#rellm-GroupPost)s of an existing [`Post`](#rellm-Post).
The ID of the post to get [`GroupPost`](#rellm-GroupPost)s for.
The ID of the group to get [`GroupPost`](#rellm-GroupPost)s for.
Used for getting context about [`GroupPost`](#rellm-GroupPost)s of an existing [`Post`](#rellm-Post).
The [`GroupPost`](#rellm-GroupPost)s for the given [`Post`](#rellm-Post) or [`Group`](#rellm-Group).
Gets Groups. *Publicly accessible **or** Authenticated.* Unauthenticated calls only return Groups of `GLOBAL_PUBLIC` visibility.
Request to get a group or groups by name or ID.
The ID of the group to get.
The name of the group to get.
The shortname of the group to get. Group shortname search is case-insensitive.
The group listing type.
The page of results to get.
Response to a GetGroupsRequest.
The groups that matched the request.
Whether there are more groups to get.
Gets MarketProducts available for purchase on this server (`market.proto`). *Unauthenticated* -- admins additionally see delisted MarketProducts.
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.
(message has no fields)
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).
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*.
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.
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.
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`).
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](#media).
Valid GetMediaRequest formats: - `{user_id: abc123}` - Gets the media of the given user that the current user can see. IE: - *all* of the current user's own media - `GLOBAL_PUBLIC` media (and `LICENSED`, if `MediaSettings.licensed_media_visible_globally`) for the user if the current user is not logged in. - `LICENSED` media too for any logged-in user. - `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. - `{}` (neither `media_id` nor `user_id`) - Browses *all* media on the server visible to the current user (never other users' `PRIVATE`/`LIMITED` media), newest first -- the basis of the Media/Video/Audio pages. Combine with `content_type` and/or `search_text`. `content_type` and `search_text` also narrow `{user_id: ...}` requests.
Returns the single media item with the given ID.
Returns all media items for the given user.
Filters by the original upload's MIME content type. Either exact (`video/mp4`) or a wildcard subtype (`audio/*`, `video/*`, `image/*`). Backed by an indexed column.
Full-text search over name, credits (artist, album, composer, ...) and description -- see `MediaMetadata`'s doc for field weighting. Prefix-matches, like `GetPosts`' `TEXT_SEARCH`. Results are ranked by match quality, then recency.
Get Members (User+Membership) of a Group. *Publicly accessible **or** Authenticated.*
Request to get members of a group.
The ID of the group to get members of.
The username of the members to search for.
The membership status to filter members by. If not specified, all members are returned.
The page of results to get.
Response to a GetMembersRequest.
The members that matched the request.
Whether there are more members to get.
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.
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.
The type of message listing to return. Required.
Returns the single message with the given ID (assuming the user has access to it).
Returns messages that are part of the given messaging group (assuming the user has access to it).
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`.
Request to only return posts that were published or created before the given timestamp.
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`](#rellm-GetMessagesRequest), containing the requested messages.
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.
Gets Posts. *Publicly accessible **or** Authenticated.* Unauthenticated calls only return Posts of `GLOBAL_PUBLIC` visibility.
Valid GetPostsRequest formats: - `{[listing_type: AllAccessiblePosts]}` - Get ServerPublic/GlobalPublic posts you can see based on your authorization (or lack thereof). - `{listing_type:MyGroupsPosts|FollowingPosts}` - Get posts from groups you're a member of or from users you're following. Authorization required. - `{post_id:}` - Get one post ,including preview data/ - `{post_id:, reply_depth: 1}` - Get replies to a post - only support for replyDepth=1 is done for now though. - `{listing_type: MyGroupsPosts|[`GroupPost`](#rellm-GroupPost)sPendingModeration, group_id:}` - Get posts/posts needing moderation for a group. Authorization may be required depending on group visibility. - `{author_user_id:, group_id:}` - Get posts by a user for a group. (TODO) - `{listing_type: AuthorPosts, author_user_id:}` - Get posts by a user. (TODO) - `{listing_type: TextSearch, search_text:}` - Full-text search across accessible posts' author username/real name, title, link, and content. - `{listing_type: TextSearch, search_text:, author_user_id:}` scopes the search to one author.
Returns the single post with the given ID.
Limits results to those by the given author user ID.
Excludes results by any of the given author user IDs. Applies to every feed-style listing (all listing types except single `post_id`/reply lookups). Numbered out of order (it logically belongs right after `author_user_id`) so as not to break compatibility; the order will be straightened out in a later protocol version.
Limits results to those in the given group ID.
Only supported for depth=2 for now.
Only POST and REPLY are supported for now.
Returns expanded posts with the given IDs.
The listing type of the request. See [`PostListingType`](#rellm-PostListingType) for more info.
The page of results to return. Defaults to 0.
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`.
Request to only return posts that were published or created before the given timestamp.
Used for getting posts.
The posts returned by the request.
Checks whether the calling user specifically (not just "some account on this browser") has a [`PushSubscription`](#rellm-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`](#grpc-api-RegisterPushSubscription)'s own doc comment), so knowing the endpoint alone isn't enough to know which of them are actually notified by it.
Checks whether the current user has already registered a given Web Push subscription endpoint. See [`GetPushSubscriptionStatus`](#grpc-api-GetPushSubscriptionStatus)'s own RPC doc comment.
The Web Push subscription endpoint URL to check, as given by `PushManager.subscribe()`.
Whether the current user has a [`PushSubscription`](#rellm-PushSubscription) registered for this exact `endpoint`.
Gets Rsvps for an Occasion. *Publicly accessible **or** Authenticated.*
Request to get RSVP data for an event.
The ID of the event to get RSVP data for.
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.) Takes the same plain / multi-occasion forms as `GetEventsRequest.anonymous_attendee_auth_token`.
Gets the Rellm server's [`ServerConfiguration`](#rellm-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.
Get the version (from Cargo) of the Rellm service. *Publicly accessible.*
Version information for the Rellm server.
The version of the Rellm server. May be suffixed with the GitHub SHA of the commit that generated the binary for the server.
Gets a user's SyncDestinations. *Authenticated* (self, or Admin for any user).
Response to a request for the current user's [`SyncDestination`](#rellm-SyncDestination)s.
The current user's SyncDestinations.
Gets a user's SyncSources. *Authenticated* (self, or Admin for any user).
Gets Users. *Publicly accessible **or** Authenticated.* Unauthenticated calls only return Users of `GLOBAL_PUBLIC` visibility.
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:}` - Full-text search across accessible users' username, real name, and bio. - `{listing_type: FOLLOWERS_TEXT_SEARCH, search_text:, user_id:}` (and the `FOLLOWING_TEXT_SEARCH`/`FRIENDS_TEXT_SEARCH`/`FOLLOW_REQUESTS_TEXT_SEARCH` equivalents) - Scopes that same full-text search to `user_id`'s followers/following/friends/follow requests, same relationship rules as the non-search `listing_type`.
The username to search for. Substrings are supported.
The user ID to search for.
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.
The page of results to return. Pages are 0-indexed.
The number of results to return per page.
Response to a [`GetUsersRequest`](#rellm-GetUsersRequest).
The users matching the request.
Whether there are more pages of results.
Grants (or resets) another user's metered access to one of the current user's AIProviders. *Authenticated*, owner-only (no Admin override).
Request to grant (or reset) another user's metered access to one of the current user's [`AIProvider`](#rellm-AIProvider)s. *Authenticated, owner-only - no Admin override.*
The user to grant access to.
The AIProvider to grant access to. Must be owned by the caller.
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.
The models the grantee is allowed to use, mirroring [`AIProviderGrant.model_names`](#rellm-AIProviderGrant) - if empty, allows access to any model the provider supports. Also replaced (not merged) on a repeat call, same as `tokens`.
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`](#rellm-ClusterResources) - 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`](#rellm-LockClusterResourcesResponse) for the polling contract this expects of callers.
See [`LockClusterResources`](#grpc-api-LockClusterResources).
This instance's own `ClusterResources.namespace_id`.
Which resources to lock - see the `ClusterResource` enum for what exists.
See [`LockClusterResources`](#grpc-api-LockClusterResources).
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.
Set only when `granted` is `false`: one of the namespaces already holding a requested (and therefore denied) resource.
Logs in a user and provides a `refresh_token` (along with an `access_token`). *Publicly accessible.*
Request to login to an existing account.
Username for the account to be logged into. Must exist.
Password for the account to be logged into.
Request an expiration time for the Auth Token returned. By default it will not expire.
(Not yet implemented.) The name of the device being used to login.
(TODO) If provided, username is ignored and login is initiated via user_id instead.
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.
*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.
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.
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.
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`](#grpc-api-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.
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`](#grpc-api-SendMessage) has one.
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.
The Messages to mark read/unread. The caller must have the same access to each of them [`GetMessages`](#grpc-api-GetMessages) would require (sender, a `messaging_group` member, a Bcc recipient, or an admin) - see [`MarkMessagesRead`](#grpc-api-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`](#rellm-MarkMessagesReadRequest) - one [`MessageRead`](#rellm-MessageRead) per `message_ids` entry, in the same order, each reflecting that message's own read/unread result (see `MarkMessagesReadRequest.unread`).
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`](#rellm-WebPushConfig) configured - there's nothing to push notifications *with*.
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`](#grpc-api-RegisterPushSubscription)'s own RPC doc comment.
The Web Push subscription endpoint URL, as given by `PushManager.subscribe()`.
The subscription's `p256dh` key (base64url), as given by `PushSubscription.getKey('p256dh')`.
The subscription's `auth` key (base64url), as given by `PushSubscription.getKey('auth')`.
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.
The ID of the subscription.
The Web Push subscription endpoint URL, as given by `PushManager.subscribe()`.
The time the subscription was registered.
Delete ALL Media, Posts, Groups and Users except the user who performed the RPC. *Authenticated.* Requires `ADMIN` permissions. Note: Server Configuration is not deleted.
Resets the current user's - or, for admins, a given user's - password. *Authenticated.*
Request to reset a password.
If not set, use the current user of the request.
The new password to set.
Revokes another user's access to one of the current user's AIProviders. *Authenticated*, owner-only (no Admin override).
Request to revoke another user's access to one of the current user's [`AIProvider`](#rellm-AIProvider)s, the reverse of [`GrantAIProvider`](#grpc-api-GrantAIProvider). *Authenticated, owner-only - no Admin override.*
The user whose access should be revoked.
The AIProvider to revoke access to. Must be owned by the caller.
Sends a Message to one or more recipients (creating/reusing their MessagingGroup). *Publicly accessible **or** Authenticated.* Like [`CreatePost`](#grpc-api-CreatePost)/[`CreateEvent`](#grpc-api-CreateEvent), authentication (if any) is via a standard `access_token`; unauthenticated calls are simply sent with no `sender`.
Request to create a new message. The server will create a new messaging group for the message, and send it to the given recipients.
Star a Post. *Unauthenticated.*
Starts SMS verification of the current user's own phone [`ContactMethod`](#rellm-ContactMethod). *Authenticated, self-only.* Requires the server to have a [`TwilioConfig`](#rellm-TwilioConfig)/[`BirdConfig`](#rellm-BirdConfig)/ [`TelnyxConfig`](#rellm-TelnyxConfig) SMS provider configured and enabled (see [`ContactProtocol`](#rellm-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`](#rellm-ContactMethod)'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.
(TODO) Reply streaming interface. Currently just streams fake example data.
Syncs (cross-posts) an Occasion to a SyncDestination. *Authenticated* (destination owner, or Admin), requires `SYNC_EVENTS_TO_FACEBOOK` (or Admin).
Syncs (cross-posts) a single Occasion to one SyncDestination.
The Occasion to sync.
The SyncDestination to sync it to.
Syncs (cross-posts) a Post to a SyncDestination. *Authenticated* (destination owner, or Admin), requires `SYNC_POSTS_TO_FACEBOOK` (or Admin).
Syncs (cross-posts) a single Post to one SyncDestination.
The Post to sync.
The SyncDestination to sync it to.
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.
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`](#grpc-api-UnregisterPushSubscription)'s own RPC doc comment.
The Web Push subscription endpoint URL to unregister, as previously passed to [`RegisterPushSubscription`](#grpc-api-RegisterPushSubscription).
Unstar a Post. *Unauthenticated.*
Updates an AIProvider's name, provider, or credentials. *Authenticated* (owner, or Admin for any user's).
Updates an Event. Automatically creates/updates/deletes child Occasions of the Event. *Authenticated.* Since Events are more complex structures, [`UpdateEventDetails`](#grpc-api-UpdateEventDetails), [`CreateNewOccasions`](#grpc-api-CreateNewOccasions), [`UpdateOccasions`](#grpc-api-UpdateOccasions), and [`DeleteRemovedOccasions`](#grpc-api-DeleteRemovedOccasions) are provided as separate RPCs to break down what happens during this request.
Updates only the [`Event`](#rellm-Event)'s top-level details and those of its [`Post`](#rellm-Post) (not any [`Occasion`](#rellm-Occasion)s or their [`Post`](#rellm-Post)s). *Authenticated.*
Used to approve follow requests. *Authenticated.*
Update a Groups's information, default membership permissions or moderation. *Authenticated.* Requires `ADMIN` permissions within the group, or `ADMIN` permissions for the user.
Group Moderators: Approve/Reject a GroupPost. *Authenticated.*
Updates a MarketProduct's amount/currency/details/delisted_at. *Authenticated*, requires Admin. `type`/`period` are immutable after creation and are ignored if changed.
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).
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 object storage 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.
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.
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.*
Updates a Post. *Authenticated.*
Updates a SyncDestination. *Authenticated* (owner, or Admin for any user's), requires `SYNC_EVENTS_TO_FACEBOOK` or `SYNC_POSTS_TO_FACEBOOK` (or Admin).
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).
Update a user by ID. *Authenticated.* Updating other users requires `ADMIN` permissions.
Upsert an Rsvp. *Publicly accessible **or** Authenticated, with anonymous RSVP support.* See [Rsvp](#rellm-Rsvp) and [AnonymousAttendee](#rellm-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).
Verifies a code sent by [`StartContactMethodVerification`](#grpc-api-StartContactMethodVerification). *Authenticated, self-only.* On match, sets [`ContactMethod.verified_at`](#rellm-ContactMethod) and clears `verification_in_progress`. Codes expire after 10 minutes and allow at most 5 attempts before requiring a fresh [`StartContactMethodVerification`](#grpc-api-StartContactMethodVerification) call.
Request for [`VerifyContactMethod`](#grpc-api-VerifyContactMethod).
The `tel:` (or, in the future, `mailto:`) value being verified -- must match the current user's own stored `phone`/`email` value.
The code the user was sent by `StartContactMethodVerification`.
`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.
Used in:
Which `AIProvider` this grant is against.
Which of that provider's models the grant covers.
The buyer's new total token balance for `ai_provider_id`/`model_names`, replacing (not adding to) whatever balance remained -- see `logic::market::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.
Used in: ,
Which `AIProvider` this grant is against.
Which of that provider's models the grant covers.
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::market_fulfillment::fulfill_purchase`'s `AiGrants` arm.
One specific model a user may call right now, and how - via an [`AIProvider`](#rellm-AIProvider) they own outright (`grant` unset), or via an [`AIProviderGrant`](#rellm-AIProviderGrant) someone else granted them (`grant` set). Only ever defined relative to a user - see [`User.ai_models`](#rellm-User)/[`GetAIProvidersResponse.ai_models`](#rellm-GetAIProvidersResponse). 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.
Used in: , ,
The exact model name to use when calling the provider (e.g. `"gemini-3.1-flash-image"`).
What this model can actually do - from the server's own hardcoded catalog for `provider.provider`'s variant (see [`AIModelCapability`](#rellm-AIModelCapability)), not anything reported by the provider's API itself. Feature gating keys off this rather than `model_name` directly, so e.g. [`GenerateMedia`](#grpc-api-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.
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).
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`](#grpc-api-GetAIProviders)'s own doc; a mere grantee never sees who else has been granted access to a provider they don't own.
What an [`AIModel`](#rellm-AIModel) can actually do - drives feature gating (e.g. [`GenerateMedia`](#grpc-api-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.
Used in:
The model's capabilities are unknown (e.g. the server doesn't know what this provider supports).
The model can generate new text from a prompt.
The model can generate a new image from a text prompt alone - what [`GenerateMedia`](#grpc-api-GenerateMedia) requires when `GenerateMediaRequest.media_ids` is empty (no reference images to edit with).
The model can edit an existing image, given a text prompt and one or more reference images -- what [`GenerateMedia`](#grpc-api-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.
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`](#rellm-SyncDestination)/[`SyncSource`](#rellm-SyncSource) (also user-owned integrations with an [`Author`](#rellm-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 [`AIProviderGrant`](#rellm-AIProviderGrant)s rather than posted-to/subscribed-from. Providers are managed via [`GetAIProviders`](#grpc-api-GetAIProviders), [`CreateAIProvider`](#grpc-api-CreateAIProvider) (requires `CREATE_AI_PROVIDERS`, or Admin), [`UpdateAIProvider`](#grpc-api-UpdateAIProvider) (owner, or Admin for any user's), and [`DeleteAIProvider`](#grpc-api-DeleteAIProvider) (owner, or Admin) - the same self-or-Admin shape as [`SyncDestination`](#rellm-SyncDestination)'s RPCs. Access to a provider is granted/revoked to other users via [`GrantAIProvider`](#grpc-api-GrantAIProvider)/[`RevokeAIProvider`](#grpc-api-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`](#rellm-GeminiCredentials)/[`OpenAICredentials`](#rellm-OpenAICredentials)/ [`DigitalOceanCredentials`](#rellm-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`](#rellm-AnthropicCredentials) is defined for forward compatibility but is not yet accepted by [`CreateAIProvider`](#grpc-api-CreateAIProvider) (Anthropic doesn't offer image generation).
Used as request type in: Rellm.CreateAIProvider, Rellm.UpdateAIProvider
Used as response type in: Rellm.CreateAIProvider, Rellm.UpdateAIProvider
Used as field type in: , ,
Unique ID for the AIProvider.
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.
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.
Identifies which external AI service this provider connects to, and carries that service's credentials. Only one variant may be set at a time. The `gemini_credentials`/`openai_credentials`/`digitalocean_credentials` variants are accepted by [`CreateAIProvider`](#grpc-api-CreateAIProvider)/[`UpdateAIProvider`](#grpc-api-UpdateAIProvider) -- see each credentials message below for why the actual key/secret is never sent back in a response.
A [Google Gemini API](https://ai.google.dev/gemini-api) connection, used for image generation/editing (e.g. generating Event posters) via its [Interactions API](https://ai.google.dev/gemini-api/docs/image-generation).
An [OpenAI API](https://platform.openai.com/docs/api-reference) connection, used for image generation/editing via its [Images API](https://platform.openai.com/docs/guides/image-generation) (GPT Image models).
An [Anthropic API](https://docs.anthropic.com) connection. *Not yet creatable* - Anthropic doesn't offer an image generation API.
A [DigitalOcean Gradient AI Platform](https://docs.digitalocean.com/products/gradient-ai-platform/) / Serverless Inference connection, used for image generation (no editing - DigitalOcean's [Serverless Inference API](https://docs.digitalocean.com/products/gradient-ai-platform/reference/api/serverless-inference/) 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).
Other users this provider's owner has granted metered access to, via [`GrantAIProvider`](#grpc-api-GrantAIProvider). Only ever populated for the owner (or an Admin) -- see [`GetAIProviders`](#grpc-api-GetAIProviders).
The time the provider was created.
The time the provider was last updated (renamed, or had its provider/credentials changed).
A grant of metered access to someone else's [`AIProvider`](#rellm-AIProvider), created/reset via [`GrantAIProvider`](#grpc-api-GrantAIProvider) and removed via [`RevokeAIProvider`](#grpc-api-RevokeAIProvider). Upserted on the unique `(ai_provider_id, ai_model_grantee)` pair - calling [`GrantAIProvider`](#grpc-api-GrantAIProvider) again for a user who already has a grant *resets* `tokens_remaining` to the newly-requested amount, it does not add to it.
Used as response type in: Rellm.GrantAIProvider
Used as field type in: ,
The ID of the [`AIProvider`](#rellm-AIProvider) this grant is for.
The user this access was granted to.
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".
The number of tokens the grantee may still spend against this provider. Set (and reset) by the owner via [`GrantAIProvider`](#grpc-api-GrantAIProvider). Once this reaches 0, [`GenerateMedia`](#grpc-api-GenerateMedia) stops working for the grantee entirely, until the owner grants more via [`GrantAIProvider`](#grpc-api-GrantAIProvider) again.
How far a single [`GenerateMedia`](#grpc-api-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`](#grpc-api-GrantAIProvider) call (any prior debt is cleared, not carried forward) - see that RPC's own doc.
The time the grant was first created.
The time the grant was last updated (i.e. last reset by another [`GrantAIProvider`](#grpc-api-GrantAIProvider) call).
An anonymous internet user who has RSVP'd to an [`Occasion`](#rellm-Occasion). (TODO:) The visibility on `AnonymousAttendee` [`ContactMethod`](#rellm-ContactMethod)s should support the `LIMITED` visibility, which will make them visible to the event creator.
Used in:
A name for the anonymous user. For instance, "Bob Gomez" or "The guy on your front porch."
Contact methods for anonymous attendees. Currently not linked to Contact methods for users.
Used to allow anonymous users to RSVP to an event. Generated by the server when an RSVP is upserted for the first time. Subsequent RSVP upserts, with the same occasion_id and anonymous_attendee.auth_token, will update existing anonymous RSVP records. Invalid auth tokens used during upserts will always create a new [`Rsvp`](#rellm-Rsvp).
Credentials for an [Anthropic API](https://docs.anthropic.com) connection. *Not yet creatable* - defined for forward compatibility only.
Used in:
The Anthropic API key. Never populated in responses (see [`GeminiCredentials.gemini_api_key`](#rellm-GeminiCredentials)).
Authentication features that can be enabled/disabled by the server admin.
Used in:
An authentication feature that is not known to the server. (Likely, the client and server use different versions of the Rellm protocol.)
Users can sign up for an account.
Users can sign in with an existing account.
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).
Used in: , , , , , , , , , , , ,
Permanent string ID for the user. Will never contain a `@` symbol.
Impermanent string username for the user. Will never contain a `@` symbol.
The user's avatar.
Bird (https://bird.com, formerly MessageBird) Config -- an alternative SMS verification provider to [`TwilioConfig`](#rellm-TwilioConfig), with a simpler single-API-key auth model. See `docs/contact_integrations.md` for full setup steps.
Used in:
The Bird workspace's API access key. Never serialized once written.
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.
Which Bird API region to call ("us1" or "eu1", per Bird's own regional API hosts). Not secret. Empty defaults to "us1".
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`). Write-only, same "always blanked to `""` once written" treatment as every other credential here -- see `TwilioConfig.twilio_webhook_signing_key`'s own doc for the full reasoning, including why storing a key here doesn't by itself turn verification on.
Whether `bird_webhook_signing_key` above is actually used to verify inbound deliveries -- same "explicit opt-in, defaults to `false`" reasoning as [`TwilioConfig.use_twilio_webhook_signing_key`](#rellm-TwilioConfig).
A Bluesky (AT Protocol) account connected as a [`SyncDestination`](#rellm-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.
Used in:
The account's handle, e.g. "jon.bsky.social".
The account's DID (decentralized identifier), populated by the server when the connection is made.
Only used (and required) on [`CreateSyncDestination`](#grpc-api-CreateSyncDestination)/[`UpdateSyncDestination`](#grpc-api-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.
A Bluesky/AT Protocol AppView clients may read public posts from anonymously. See `FederationInfo.bluesky_servers`. AT Protocol splits "a server" into several roles: a Personal Data Server (PDS) stores an account's data and handles its login, while an AppView serves the read side (feeds, threads, profiles). Every AppView indexes the same global network, so an anonymous "Bluesky server" in Rellm's UI is really an AppView - the public one run by Bluesky PBC is `public.api.bsky.app`, which allows unauthenticated reads (with CORS) of public data. A connected *account* is separate: it logs in at its own PDS (not necessarily `bsky.social`), which proxies reads to an AppView on the account's behalf.
Used in:
The AppView's API hostname, e.g. "public.api.bsky.app" (shown to users as "bsky.app").
Indicates to UI clients that they should add the indicated AppView to the user's browsed Bluesky sources by default (switched off unless also `pinned_by_default`).
Indicates to UI clients that they should add *and enable* the indicated AppView by default (showing its public feed alongside the "main" server). Only one Bluesky source (an AppView or a connected account) is shown at a time, so clients should ignore all but the first of these.
URL of an image to show as this AppView's logo in clients' server lists (an `https://` URL; clients ignore anything else). Unlike a Mastodon instance (`GET /api/v1/instance` has a `thumbnail`) or a Rellm server (which carries its own `ServerLogo`), an AppView has no way to describe itself, so there is nothing to discover one from - it has to be provided here. Clients show a built-in logo for Bluesky's own public AppView (`public.api.bsky.app`) when this is unset, and a plain placeholder for any other AppView.
The Events Calendar's default UI granularity.
Used in: ,
Shows a 7-day week at a time. Good default for most servers.
Shows a full month at a time. Better for servers with fewer events.
Shows a single day at a time. Better for servers with many events.
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`.
Used in:
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.
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.
A resource `ClusterResources.conductor_host` can hand out an exclusive, cluster-wide lock on via [`LockClusterResources`](#grpc-api-LockClusterResources)/ [`FreeClusterResources`](#grpc-api-FreeClusterResources).
Used in: , , ,
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.
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`).
Used in:
The number of distinct namespaces that may concurrently hold a lock on `resource`.
One namespace's currently-held lock on one or more `ClusterResource`s, 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.
Used in:
The `namespace_id` (see `ClusterResources.namespace_id`) holding this lock.
Which resources this lock covers.
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`](#rellm-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`](#grpc-api-LockClusterResources)/ [`FreeClusterResources`](#grpc-api-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.
Used in:
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."
DNS hostname of whichever instance in the cluster is the "conductor" - the single instance that actually brokers [`LockClusterResources`](#grpc-api-LockClusterResources)/ [`FreeClusterResources`](#grpc-api-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`](#http-based-client-host-negotiation-for-external-cdns-get-backend_host) first, falling back to `conductor_host` itself - rather than connecting to it directly, in case the conductor sits behind an [`ExternalCDNConfig`](#rellm-ExternalCDNConfig).
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`](#rellm-FacebookAuthConfig)/ [`WebPushConfig.private_vapid_key`](#rellm-WebPushConfig) - `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.
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`](#grpc-api-ConfigureServer) never lets a caller change this, and it isn't versioned the way other `ConfigureServer` changes are.
A single entry in a [`ContactMethod.consent_history`](#rellm-ContactMethod), 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`](#rellm-ContactMethod) is just a denormalized copy of it, kept for convenient access without walking the history.
Used in:
The [`ContactConsentState`](#rellm-ContactConsentState) the `ContactMethod` was changed to.
When this change took effect. Always the server's own time as of the [`UpdateUser`](#grpc-api-UpdateUser) call that made the change -- any `changed_at` sent by a client is ignored.
Whether a user has consented to being contacted (e.g. via SMS/email sent by external services) through a given [`ContactMethod`](#rellm-ContactMethod). See [`ContactMethod.consent_state`](#rellm-ContactMethod).
Used in: ,
The user has not consented, or has revoked a prior consent. External services may not use this `ContactMethod` to contact the user.
The user currently consents to being contacted via this `ContactMethod`.
A contact method for a user (`tel:` or `mailto:`). SMS verification via [`StartContactMethodVerification`](#grpc-api-StartContactMethodVerification)/ [`VerifyContactMethod`](#grpc-api-VerifyContactMethod), backed by whichever of [`TwilioConfig`](#rellm-TwilioConfig)/[`BirdConfig`](#rellm-BirdConfig)/ [`TelnyxConfig`](#rellm-TelnyxConfig) the server has enabled -- see `supported_by_server` below, and [`ContactProtocol`](#rellm-ContactProtocol) for the corresponding server-wide toggle. `mailto:` has no verification provider yet.
Used as request type in: Rellm.StartContactMethodVerification
Used as response type in: Rellm.StartContactMethodVerification, Rellm.VerifyContactMethod
Used as field type in: , ,
Either a valid `mailto:` or valid `tel:` URL.
The visibility of the contact method.
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`](#rellm-ContactProtocol) currently listed in `ServerConfiguration.supported_contact_protocols` (see the [`ServerConfiguration`](#rellm-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.
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`](#grpc-api-VerifyContactMethod) on a correct code.
Set while an SMS verification code has been sent and not yet confirmed, expired, or exhausted -- populated by [`StartContactMethodVerification`](#grpc-api-StartContactMethodVerification) and cleared by a successful [`VerifyContactMethod`](#grpc-api-VerifyContactMethod) (which sets `verified_at` instead) or by expiry/too-many-attempts. See [`ContactMethodVerification`](#rellm-ContactMethodVerification) for its own fields.
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`](#grpc-api-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`](#grpc-api-UpdateUser) RPC -- same self-or-`ADMIN` gate as `value`/ `visibility` (see `update_user.rs`'s `admin || self_update` check), not restricted further; every change appends a new entry to `consent_history` (server-timestamped, never trusted from client input). Only ever sent to the `ContactMethod`'s own owner or an `ADMIN` -- unlike `value`, never relaxed by `VIEW_PRIVATE_CONTACT_METHODS`; every other viewer sees this blanked to `CONTACT_CONSENT_REVOKED` regardless of the `ContactMethod`'s own `visibility`.
Append-only history of every `consent_state` change, oldest first. Not directly modifiable -- the server appends to it whenever [`UpdateUser`](#grpc-api-UpdateUser) changes `consent_state`, using the server's own time for [`ContactConsentChange.changed_at`](#rellm-ContactConsentChange) 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`](#rellm-ContactMethod). Verification cannot begin until contact consent (`ContactMethod.consent_state`) is granted -- see [`StartContactMethodVerification`](#grpc-api-StartContactMethodVerification).
Used in:
Never serialized to gRPC by the backend. Only stored server-side; a client's own attempt to verify goes through [`VerifyContactMethodRequest.code`](#rellm-VerifyContactMethodRequest) instead, not this field.
Number of failed [`VerifyContactMethod`](#grpc-api-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.
The two contact schemes [`ContactMethod.value`](#rellm-ContactMethod) (`users.proto`) may take -- see [`ServerConfiguration.supported_contact_protocols`](#rellm-ServerConfiguration) for the server-wide setting keyed off this enum, and `docs/contact_integrations.md` for the full picture.
Used in:
`tel:` (phone/SMS) contact. Requires an enabled [`TwilioConfig`](#rellm-TwilioConfig)/ [`BirdConfig`](#rellm-BirdConfig)/[`TelnyxConfig`](#rellm-TelnyxConfig).
`mailto:` (email) contact. Currently not supported -- no email provider exists yet.
The SMS providers [`ServerConfiguration.preferred_verification_apis`](#rellm-ServerConfiguration)/ [`available_verification_apis`](#rellm-ServerConfiguration) order between -- [`TwilioConfig`](#rellm-TwilioConfig), [`BirdConfig`](#rellm-BirdConfig), and [`TelnyxConfig`](#rellm-TelnyxConfig). See `contact_verification.rs`'s own module doc for the preference-then-fallback logic these values drive.
Used in:
Request to create a new third-party refresh token. Unlike [`LoginRequest`](#rellm-LoginRequest) or [`CreateAccountRequest`](#rellm-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: 1. When the `bullcity.social` user wants to login on `jonline.io`, `bullcity.social` will redirect the user to `jonline.io/third_party_auth?to=bullcity.social`. 2. `jonline.io` will force the user to login if needed on this page. 3. `jonline.io` will prompt/warn the user, and then call this RPC to create a refresh + access token for `bullcity.social`. 4. `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`](#rellm-RefreshTokenResponse) message.) 6. `bullcity.social` will ensure it can [`GetCurrentUser`](#grpc-api-GetCurrentUser) on `jonline.io` with its new auth token. 5. `bullcity.social` will replace the current location with `bullcity.social/third_party_auth?from=jonline.io`. 7. `bullcity.social` will use the access token to make requests to `jonline.io` (the same as with `bullcity.social`). Note that refresh tokens
The third-party refresh token's expiration time.
The third-party refresh token's user ID.
The third-party refresh token's device name.
Custom CSS for a Rellm server's Elm SPA, plus the appearance settings that go with it. Stored alongside the [`ServerConfiguration`](#rellm-ServerConfiguration) (so it is versioned the same way, with each [`ConfigureServer`](#grpc-api-ConfigureServer) or [`ConfigureCustomCSS`](#grpc-api-ConfigureCustomCSS) creating a new configuration version) and served to browsers as a stylesheet at `/custom_css.css` (`/elm/custom_css.css` when the Elm SPA is served under `/elm`). The stylesheet always starts with a `:root` block defining CSS variables for custom CSS to use: `--primary-color` and `--nav-color` (the server's configured `ServerColors.primary` and `ServerColors.navigation`, as `#rrggbb`), and `--custom-media-1`, `--custom-media-2`, ... (see `media_ids`). The potentially large `custom_css` text is only ever returned by [`GetCustomCSS`](#grpc-api-GetCustomCSS) and [`ConfigureCustomCSS`](#grpc-api-ConfigureCustomCSS): [`GetServerConfiguration`](#grpc-api-GetServerConfiguration) includes this message as `ServerConfiguration.custom_css_configuration` but with `custom_css` unset, and never reads it from the database. [`ConfigureServer`](#grpc-api-ConfigureServer) ignores any `custom_css_configuration` it is sent, always leaving the stored one unchanged.
Used as request type in: Rellm.ConfigureCustomCSS
Used as response type in: Rellm.ConfigureCustomCSS, Rellm.GetCustomCSS
Used as field type in:
These media IDs will be converted to the CSS vars `--custom-media-1`, `--custom-media-2`, etc., in the order they are listed (each a `url("...")`, so `background: var(--custom-media-1)` works as is). The vars `--primary-color` and `--nav-color` are always available too -- see above. Each must be `GLOBAL_PUBLIC` Media, so anonymous visitors can load it. At most 32.
Custom CSS applied to the Elm SPA client. This is appended to the default CSS, so it can override any default styles. At most 64 KiB. Unset in `GetServerConfiguration`'s copy of this message; in a `ConfigureCustomCSS` request, unset means "leave the stored CSS as it is" (so the other fields can be changed without sending it), while an empty string clears it.
Force the Elm SPA into its light theme for everyone, whatever their own Auto/Light/Dark setting or system preference -- the theme toggles are disabled. For stylesheets (such as a "paper" or "polaroid" look) that only work on a light background. At most one of `force_light_theme` and `force_dark_theme` may be set.
Same as `force_light_theme`, for the dark theme.
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).
Used in:
What `/` renders. Only `HOME_TAB` (the default, combined Events+Posts feed), `EVENTS_TAB`, or `POSTS_TAB` are valid here - never `PEOPLE_TAB`/`ABOUT_TAB`.
Renders a specific Post at `/` instead (e.g. for a custom business site's landing page).
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).
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).
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.
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`.
Used in:
Links to one of the app's predefined tabs/pages.
Links to a specific Post (e.g. for a custom business site's page).
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 shown as the tab's icon (e.g. "🎪").
Media ID (see [`Media`](#rellm-Media) APIs) of an image shown as the tab's icon.
Title shown for the tab. Defaults to the predefined tab's/Post's title if unset.
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`, `about`, `media`, `video`, `videos`, `audio`, and `images` 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.
Used in:
Overrides the default `/` page. If unset, the default combined Events+Posts feed is used.
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`](#rellm-CustomNavigationTab).path's own doc. `/` itself is overridden via `home` above instead.
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.
Credentials for a [DigitalOcean Gradient AI Platform](https://docs.digitalocean.com/products/gradient-ai-platform/) / Serverless Inference connection, accepted by [`CreateAIProvider`](#grpc-api-CreateAIProvider)/[`UpdateAIProvider`](#grpc-api-UpdateAIProvider). Used for image *generation only* (no editing - see `AIProvider.provider`'s own doc on this variant) via its [Serverless Inference API](https://docs.digitalocean.com/products/gradient-ai-platform/reference/api/serverless-inference/) `/v1/images/generations` endpoint, OpenAI-Images-API-shaped and re-hosting GPT Image and Stable Diffusion models -- see [`GenerateMedia`](#grpc-api-GenerateMedia).
Used in:
The DigitalOcean Serverless Inference API token. Required (and only used) on [`CreateAIProvider`](#grpc-api-CreateAIProvider)/[`UpdateAIProvider`](#grpc-api-UpdateAIProvider) -- never populated in responses (see [`GeminiCredentials.gemini_api_key`](#rellm-GeminiCredentials)).
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.)
Used as request type in: Rellm.CreateEvent, Rellm.CreateNewOccasions, Rellm.DeleteEvent, Rellm.DeleteRemovedOccasions, Rellm.UpdateEvent, Rellm.UpdateEventDetails, Rellm.UpdateOccasions
Used as response type in: Rellm.CreateEvent, Rellm.CreateNewOccasions, Rellm.DeleteEvent, Rellm.DeleteRemovedOccasions, Rellm.UpdateEvent, Rellm.UpdateEventDetails, Rellm.UpdateOccasions
Used as field type in:
The Post containing the underlying data for the event (title, content, moderation, visibility, etc.). Its [`PostContext`](#rellm-PostContext) should be `EVENT`. An `Event`'s ID *is* its `post.id` - there is no separate surrogate ID.
Event configuration like whether to allow (anonymous) RSVPs, etc.
A list of occasions for the Event. *Events will only include all occasions if the request is for a single event.*
The total number of occasions this Event has that the viewer can see, regardless of how many are included in `occasions` (listings only include occasions matching their time filter).
To be used for ticketing, RSVPs, etc. Stored as JSON in the database.
Used in:
Whether to allow RSVPs for the event. New RSVPs are refused (`PERMISSION_DENIED`, `rsvps_not_allowed`) unless this is true (or an Occasion overrides it); existing RSVPs can still be updated.
Whether to allow anonymous RSVPs for the event. No effect unless `allows_rsvps` is true. New anonymous RSVPs are otherwise refused (`PERMISSION_DENIED`, `anonymous_rsvps_not_allowed`).
Limit the max number of attendees. No effect unless `allows_rsvps` is true. Enforced by `UpsertRsvp`: only `GOING` RSVPs with passing moderation (`UNMODERATED`/`APPROVED`) count, by `Rsvp.number_of_guests`. An RSVP that would exceed the limit - whether new, grown, or being approved by the event owner - fails with `FAILED_PRECONDITION` (`attendee_limit_reached`). An Occasion's `OccasionRsvpInfo.max_attendees` overrides this.
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`](#rellm-Location) until the user has RSVP'd. Location will always be returned in `Rsvps` if the request for the `Rsvps` came from a (logged in or anonymous) user whose RSVP is approved (or the event owner).
Default moderation for new RSVPs from logged-in users other than the event owner (either `PENDING` or `APPROVED`). Anonymous RSVPs are always moderated (default to `PENDING`).
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`).
Used in:
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.
Returns events from users the user is following.
Returns events from any group the user is a member of.
Returns `DIRECT` events that are directly addressed to the user.
Returns events pending moderation by the server-level mods/admins.
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.
Returns events from a specific group. Requires group_id parameterRequires group_id parameter
Returns pending_moderation events from a specific group. Requires group_id parameter and user must have group (or server) admin permissions.
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.
Specific settings for Events.
Used in:
Hide the Events tab from the user with this flag.
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.
Only `SERVER_PUBLIC` and `GLOBAL_PUBLIC` are valid. `GLOBAL_PUBLIC` is only valid if default_user_permissions contains `GLOBALLY_PUBLISH_[USERS|GROUPS|POSTS|EVENTS]` as appropriate.
Can be used to rename, e.g., "Event" to "Gig" or "Performance"
Can be used to rename, e.g. "Events" to "Show," "Game," "Competition"
Works the same as for Posts.
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.
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`.
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`.
Generic type for refresh and access tokens.
Used in: ,
The secure token value.
Optional expiration time for the token. If not set, the token will not expire.
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.
Used in:
The domain where the frontend is hosted. For example, jonline.io. Typically your CDN (like Cloudflare) should own the DNS for this domain.
The domain where the backend is hosted. For example, jonline.io.itsj.online. Typically your Kubernetes provider should own DNS for this domain.
(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.
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.
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.
(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.)
Facebook authentication configuration for the server.
Used in:
The Facebook App ID for the server.
The Facebook App Secret for the server. *Never serialized to the client.* Admins: Edit this in the database's JSONB column directly.
A Facebook Page connected as a [`SyncDestination`](#rellm-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).
Used in:
The Facebook Page's ID.
The Facebook Page's name, populated by the server when the connection is made.
Only used (and required) on [`CreateSyncDestination`](#grpc-api-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.
Settings for a feature (e.g. People, Groups, Posts, Events, Media). Encompasses both the feature's visibility and moderation settings.
Used in:
Hide the Posts or Events tab from the user with this flag.
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.
Only `SERVER_PUBLIC` and `GLOBAL_PUBLIC` are valid. `GLOBAL_PUBLIC` is only valid if default_user_permissions contains `GLOBALLY_PUBLISH_[USERS|GROUPS|POSTS|EVENTS]` as appropriate.
Can be used to rename, e.g., "Person" to "Contributor" or "Group" to "Community"
Can be used to rename, e.g. "Groups" to "Subtwaddits" or "People" to "Folks"
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.
Used as request type in: Rellm.DefederateProfile, Rellm.FederateProfile
Used as response type in: Rellm.FederateProfile
Used as field type in:
The DNS hostname of the server that this user is on.
The user ID of the user on the server.
A server that this server will federate with.
Used in:
The DNS hostname of the server to federate with.
Indicates to UI clients that they should enable/configure the indicated server by default.
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.
Used in:
A list of servers that this server will federate with.
If true, this server permits cross-server ("Sign in from...") federated auth to hand tokens to a plaintext `http://localhost` frontend, for local development. Frontends should otherwise refuse to send/receive federated auth tokens for `localhost`, and should show a warning to the user whenever they do proceed. Unset (including on servers predating this field) means false. Note this is only a frontend-enforced measure: the backend can't stop a modified client from moving its own tokens wherever it likes.
Facebook authentication configuration for the server. If set, allows users to create Facebook (and Instagram) SyncDestinations for their Posts and Occasions.
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`](#rellm-XTwitterAccount) SyncDestinations always fail with `x_twitter_app_not_configured`.
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.)
Bluesky/AT Protocol AppViews clients may read public posts from without any account - see [`BlueskyServer`](#rellm-BlueskyServer). Unlike `mastodon_servers`, there are no secrets here: AT Protocol has no per-client app registration, so this list is purely a "which AppViews should clients browse by default" recommendation, mirroring `servers`' `configured_by_default`/ `pinned_by_default`.
Model for a user's follow of another user.
Used as request type in: Rellm.CreateFollow, Rellm.DeleteFollow, Rellm.UpdateFollow
Used as response type in: Rellm.CreateFollow, Rellm.UpdateFollow
Used as field type in:
The follower in the relationship.
The user being followed.
Tracks whether the target user needs to approve the follow.
The time the follow was created.
The time the follow was last updated.
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).
Used in:
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.
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).
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.
When this note was added. Server-stamped -- `UpdateMarketSubscription` always ignores whatever timestamp the client sends for a newly-appended entry.
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.
Used in: ,
The starting state for every new order -- no admin has looked at it yet.
The order is fully stood up -- set via "Add and Mark as Fulfilled" on `/market/fulfillment` (`UpdateMarketSubscription`).
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.
Credentials for a [Google Gemini API](https://ai.google.dev/gemini-api) connection - the only [`AIProvider.provider`](#rellm-AIProvider) variant currently accepted by [`CreateAIProvider`](#grpc-api-CreateAIProvider)/[`UpdateAIProvider`](#grpc-api-UpdateAIProvider). Used for image generation/editing via Gemini's [Interactions API](https://ai.google.dev/gemini-api/docs/image-generation), e.g. to generate/edit Event posters from an Event's own content - see [`GenerateMedia`](#grpc-api-GenerateMedia).
Used in:
The Gemini API key. Required (and only used) on [`CreateAIProvider`](#grpc-api-CreateAIProvider)/[`UpdateAIProvider`](#grpc-api-UpdateAIProvider) -- **never populated in responses**, the same write-only convention as e.g. [`MastodonAccount.access_token`](#rellm-MastodonAccount) in `sync.proto`.
Used in:
The "user facing" view backing /market -- the caller's own MarketSubscriptions only.
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).
`Group`s are a way to organize users and posts (and thus events). They can be used for many purposes,
Used as request type in: Rellm.CreateGroup, Rellm.DeleteGroup, Rellm.UpdateGroup
Used as response type in: Rellm.CreateGroup, Rellm.UpdateGroup
Used as field type in:
The group's unique ID.
Mutable name of the group. Must be unique, such that the derived `shortname` is also unique.
Immutable shortname of the group. Derived from changes to `name` when the [`Group`](#rellm-Group) is updated.
A description of the group.
An avatar for the group.
The default permissions for new members of the group.
The default moderation for new members of the group. Valid values are PENDING (requires a moderator to let you join) and UNMODERATED.
The default moderation for new posts in the group.
The default moderation for new events in the group.
LIMITED visibility groups are only visible to members. PRIVATE groups are only visibile to users with the ADMIN group permission.
The number of members in the group.
The number of posts in the group.
The number of events in the group.
The permissions given to non-members of the group.
The membership for the current user, if any.
The time the group was created.
The time the group was last updated.
The type of group listing to get.
Used in:
Get all groups (visible to the current user).
Get groups the current user is a member of.
Get groups the current user has requested to join.
Get groups the current user has been invited to.
A `GroupPost` is a cross-post of a [`Post`](#rellm-Post) to a [`Group`](#rellm-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.
Used as request type in: Rellm.CreateGroupPost, Rellm.DeleteGroupPost, Rellm.UpdateGroupPost
Used as response type in: Rellm.CreateGroupPost, Rellm.UpdateGroupPost
Used as field type in: ,
The ID of the group this post is in.
The ID of the post.
**Deprecated.** Prefer to use `shared_by`. The ID of the user who cross-posted the post.
The moderation of the post in the group.
The time the post was cross-posted.
Author info for the user who cross-posted the post.
An Instagram Business/Creator account connected as a [`SyncDestination`](#rellm-SyncDestination) - **never a personal Instagram account**. Unlike [`FacebookPage`](#rellm-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`](#rellm-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.
Used in:
The Instagram Business/Creator account's ID, used for all Graph API posting calls.
The Instagram account's @username, populated by the server when the connection is made.
The linked Facebook Page's ID, kept for reference/reconnect.
Only used (and required) on [`CreateSyncDestination`](#grpc-api-CreateSyncDestination): a short-lived user access token from client-side Facebook Login (same flow as [`FacebookPage`](#rellm-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.
Grants `licensed_to` access to a `LICENSED` [`Media`](#rellm-Media) item's full-quality bytes. Stored in the `media_licenses` table. A license is *active* while `revoked_at` is unset. (Purchasing licenses through Rellm's Market is not implemented yet.)
The user holding the license to the media.
The licensed media.
When set, the license no longer grants access.
Locations are places where events can happen.
Used in: ,
The ID of the location. May not be unique.
The User ID of the location's creator, if available.
This should probably come from OpenStreetMap APIs, with an option for Google Maps. Ideally both the Flutter and React apps, and any others, should prefer OpenStreetMap but give the user the option to use Google Maps.
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.
Used in:
The amount actually charged, in the same unit `MarketProduct.amount` uses (smallest unit of `currency`, except for a zero-decimal currency like JPY).
The ISO 4217 numeric currency code `amount` is denominated in -- copied from the purchase's own product at charge time.
The card actually charged, if known/resolvable at the time this MarketPayment was recorded.
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.
Used in:
E.g. "visa", "mastercard", "amex".
Last 4 digits of the card number.
1-12.
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).
Used as request type in: Rellm.CreateMarketProduct, Rellm.UpdateMarketProduct
Used as response type in: Rellm.CreateMarketProduct, Rellm.UpdateMarketProduct
Used as field type in: , ,
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.
How often this product bills, if at all -- see `PurchasePeriod`'s own doc. Never changeable after product creation, same reasoning as `type` above.
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_payments::is_zero_decimal_currency`).
The ISO 4217 numeric currency code this product is priced in (e.g. `840` for USD, `392` for JPY) -- see `logic::market::market_summary`'s currency table for the full set of currencies a server actually supports pricing in today.
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`.
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.
Type-specific configuration for this product, matching `type` above -- e.g. how much media storage a `PURCHASE_TYPE_MEDIA_STORAGE` product grants, or which AI provider/models a `PURCHASE_TYPE_AI_GRANTS` product grants tokens for. Copied onto every `MarketPurchase`/ `MarketSubscription` created from this product at the time of purchase (as that purchase's own `details`), so a later edit to this product's own `details` never retroactively changes what an existing buyer already bought.
When this product was created. Server-stamped -- `CreateMarketProduct` ignores any client-sent value.
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::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.
Used in:
Who bought this.
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.
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 `MarketPayment`s are attached, not here.
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.
Every payment recorded against this purchase, oldest first -- ordinarily just one, but a failed charge that's later retried (see `logic::market::market_renewal`) can leave more than one row.
Every refund recorded against this purchase, oldest first -- empty for the common case of a purchase that was never refunded.
Type-specific details for this purchase, matching `type` above -- copied from the `MarketProduct.details` (or the buyer-supplied `RellmHostingPurchaseDetails`, for `PURCHASE_TYPE_RELLM_HOSTING`) at the moment this purchase was fulfilled, frozen from then on even if the originating product later changes. See `logic::market::market_fulfillment::fulfill_purchase` for what each variant actually does to the buyer's account.
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.
Used in:
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).
The ISO 4217 numeric currency code `amount` is denominated in -- always matches the `MarketPayment.currency` being refunded.
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.
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.
Used in:
E.g. "visa", "mastercard", "amex".
Last 4 digits of the card number.
1-12.
4-digit year.
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.
Used in:
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.
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.
Used as request type in: Rellm.CancelMarketSubscription, Rellm.UpdateMarketSubscription
Used as response type in: Rellm.CancelMarketSubscription, Rellm.UpdateMarketSubscription
Used as field type in: , ,
Who owns this subscription (i.e. who's being billed and who the entitlement applies to).
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.
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.
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.
The ISO 4217 numeric currency code `amount` is denominated in -- copied from `market_product.currency` at the time this subscription was created.
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.
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.
Type-specific details for this subscription, matching `type` above -- copied from the originating `MarketProduct.details` at creation time (or, for `PURCHASE_TYPE_RELLM_HOSTING`, built from the buyer's own checkout-time input -- see `RellmHostingPurchaseDetails`'s own doc for the same caveat that applies here), frozen from then on even if the product later changes. Re-applied on every renewal via `logic::market::market_fulfillment::fulfill_purchase`, and read back by `logic::market::market_fulfillment::terminate_entitlement` to know exactly what to claw back on cancellation.
When this subscription was first created (i.e. when the initial `MarketPurchase` was fulfilled).
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.
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).
The time permissions were removed, media storage quotas reset, etc. -- i.e. when `logic::market::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.
A Mastodon account connected as a [`SyncDestination`](#rellm-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.
Used in:
The Mastodon instance's hostname, e.g. "mastodon.social".
The account's username on that instance, populated by the server when the connection is made.
Only used (and required) on [`CreateSyncDestination`](#grpc-api-CreateSyncDestination)/[`UpdateSyncDestination`](#grpc-api-UpdateSyncDestination): the user's own Personal Access Token for `instance_host`. Never populated in responses.
A Mastodon instance this server has a registered OAuth app on. See `FederationInfo.mastodon_servers`.
Used in:
The Mastodon instance's hostname, e.g. "mastodon.social".
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.
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.
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).
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.
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: - **Headers**: - `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. - **Query Parameters**: - `authorization` - Rellm Access Token for the user. May also be supplied in the `Cookies` or `Authorization` headers. - Fetching media without authentication requires that it has `GLOBAL_PUBLIC` visibility (or `LICENSED`, if `MediaSettings.licensed_media_visible_globally` is set). Visibility is otherwise enforced like `GetMedia`'s: `SERVER_PUBLIC` for any logged-in user, `LIMITED` for followers of the owner, `PRIVATE` for the owner and admins only. A media item the viewer can't see responds `404`. - Anonymous (unauthenticated) responses omit CORS `Access-Control-Allow-*` headers when the server's `MediaSettings.block_cors_anonymous_media_access` is set, so third-party sites can embed (`<img>`, `<video>`) but not script-read public media. - `LICENSED` media's full-quality bytes are only served to its owner, admins, and users holding an active [`License`](#rellm-License) for it. Everyone else is served its `UNLICENSED_PREVIEW_MEDIUM` conversion (audio/video only; see `MediaMetadata.unlicensed_preview_start_ms`), or `403 Forbidden` if none exists yet.
Used as request type in: Rellm.DeleteMedia, Rellm.DeleteMediaSizes, Rellm.UpdateMedia
Used as response type in: Rellm.DeleteMediaSizes, Rellm.GenerateMedia, Rellm.UpdateMedia
Used as field type in:
The ID of the media item.
The user who created the media item.
An optional title for the media item.
An optional description for the media item.
Visibility of the media item.
Moderation of the media item.
Indicates the media was generated by the server rather than uploaded manually by a user.
Media is generally stored as-is on upload. When background jobs process and compress the media, this flag is set to true.
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}`.
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.
Which stored copy of a `Media` item's bytes a `MediaSize` represents.
Used in:
The untouched original upload.
Resized to fit within 320x320 px, preserving aspect ratio (never upscaled).
Resized to fit within 800x800 px, preserving aspect ratio (never upscaled).
Resized to fit within 1600x1600 px, preserving aspect ratio (never upscaled).
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.
Same as `VIDEO_PREVIEW_THUMBNAIL_SMALL`, but resized to fit within 800x800 px -- the `MEDIA_CONVERSION_MEDIUM` tier.
Same as `VIDEO_PREVIEW_THUMBNAIL_SMALL`, but resized to fit within 1600x1600 px -- the `MEDIA_CONVERSION_LARGE` tier.
For audio media only: an `image/png` waveform image of the whole file (rendered via `ffmpeg`'s `showwavespic` filter, transparent background), 320x320 px (square, so clients can squash it vertically to suit their layout) -- same width tier as `MEDIA_CONVERSION_SMALL`. Audio has no pixel dimensions of its own, so unlike the video/image sizes these are *not* "fit within NxN": they're always exactly the tier's size. Regenerated by the same `convert_media_sizes` background job.
Same as `AUDIO_PREVIEW_THUMBNAIL_SMALL`, but 800x800 px -- the `MEDIA_CONVERSION_MEDIUM` tier.
Same as `AUDIO_PREVIEW_THUMBNAIL_SMALL`, but 1600x1600 px -- the `MEDIA_CONVERSION_LARGE` tier.
For audio and video media only: a medium-quality crop of the original from `MediaMetadata.unlicensed_preview_start_ms` to `unlicensed_preview_end_ms` (via `ffmpeg`), in the original's own container-family content type (video: `video/mp4`, audio: `audio/mpeg`). This is what viewers *without* a [`License`](#rellm-License) are served for `LICENSED` media. Regenerated by the `convert_media_sizes` background job; deleted (and marked for regeneration) whenever `UpdateMedia` changes either bound. May also be deleted manually via `DeleteMediaSizes`. (Numbered 13 rather than 11 because `AUDIO_PREVIEW_THUMBNAIL_MEDIUM` already holds 11.)
For audio media only, and only when the file has embedded cover art (an ID3 `APIC` frame, a FLAC/Vorbis picture, an MP4 `covr` atom, ...): the cover image extracted via `ffmpeg`, as `image/jpeg`, resized to fit within 320x320 px, preserving aspect ratio (never upscaled) -- same dimension tier as `MEDIA_CONVERSION_SMALL`. Unlike the `AUDIO_PREVIEW_THUMBNAIL_*` waveforms, these are the artist's actual artwork, so clients should prefer them over a waveform for list/grid thumbnails and the player's artwork (waveforms remain for the playback progress display). Absent when the file has no embedded art. Generated by the `convert_media_sizes` background job; also servable to viewers without a license for `LICENSED` media (like waveforms). Request one via `GET /media/{id}?size=audio_cover_art_small|medium|large`.
Same as `AUDIO_COVER_ART_SMALL`, but fit within 800x800 px -- the `MEDIA_CONVERSION_MEDIUM` tier.
Same as `AUDIO_COVER_ART_SMALL`, but fit within 1600x1600 px -- the `MEDIA_CONVERSION_LARGE` tier.
Free-form metadata about a [`Media`](#rellm-Media) item that isn't queried/filtered on, so doesn't need its own columns.
Used in: ,
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.
Credits. All free-form text; unset/blank means "none". Editable via `UpdateMedia` (setting one to an empty string clears it). Searched by `GetMediaRequest.search_text`, most to least important: name/artist/director, then album/composer/starring/cast/narrator, then producer/crew/publisher, then description.
For audio and video media only: the range of the original (in milliseconds) that is cropped into the `UNLICENSED_PREVIEW_MEDIUM` conversion served to viewers without a license for `LICENSED` media. Unset start defaults to 0; unset end defaults to start + 30s (clamped to the media's duration). Changing either invalidates (deletes) any existing `UNLICENSED_PREVIEW_MEDIUM` size so the `convert_media_sizes` background job regenerates it.
For audio media: the ID of another, *image* `Media` item to show as this track's cover art, overriding the art embedded in the file (`AUDIO_COVER_ART_*`). Must be an image belonging to the caller (or the caller must be an Admin); validated by `UpdateMedia`. Clients render it as `GET /media/{cover_art_media_id}?size=medium`, so only viewers who can see that image itself get it (others fall back to the embedded art/placeholder) -- give it a visibility at least as open as the track's. Unset/blank clears it.
For audio media: the track's tempo, in beats per minute -- at its start (`start_bpm`), at its end (`end_bpm`), and the slowest and fastest it gets anywhere in the track (`min_bpm`/`max_bpm`). For a steady track all four are about equal. Each must be finite, greater than 0 and at most 999, and `min_bpm` must not exceed `max_bpm`, else `UpdateMedia` fails with `INVALID_ARGUMENT` (`invalid_start_bpm`/`invalid_end_bpm`/`invalid_min_bpm`/`invalid_max_bpm`/`min_bpm_exceeds_max_bpm`). Fractions are fine (`127.5`). Unset clears it. (`start_bpm`/`end_bpm` aren't required to fall within `min_bpm`..`max_bpm`; clients may treat that as a hint to fix one.) Seeded when the track is first converted (`convert_media_sizes`): from the file's own BPM tag (ID3 `TBPM`/Vorbis `BPM`, which sets all four) if it has one, otherwise estimated from the audio -- the first and last 30s (silence trimmed) analyzed for start/end, and sliding 20s windows across the whole track for min/max. Estimates are best-effort: half/double time mistakes are the usual failure, so owners can correct them via `UpdateMedia`. Only ever filled while unset; an owner's edit (or clearing) is never overwritten by a later conversion.
For audio media: the track's musical key at its start (`start_key`) and end (`end_key`) -- equal for a track that doesn't modulate. Each is formatted `<tonic><accidental?><mode?>`: - tonic: one uppercase letter `A`-`G`; - accidental (optional): a sharp or flat, any of sharp: `#` (ASCII), `♯` (U+266F), `#` (U+FF03), `﹟` (U+FE5F) flat: `b` (ASCII lowercase), `♭` (U+266D) optionally followed by the emoji variation selector U+FE0F (what emoji keyboards insert, e.g. `♭️`); - mode (optional): `m` or `-` for minor, `M` for major; no mode means major. E.g. `C#m`, `DbM`, `Db`, `F`, `Am`, `B♭-`, `F♯m`. Case matters (`am` and `db` are invalid), and `b` is only a flat *after* the tonic letter (`Bb` = B-flat; `B` = B). At most one accidental: double sharps/flats (`C##`, `Dbb`, `𝄪`, `𝄫`) aren't accepted, nor are naturals (`♮`) -- omit the accidental. Stored exactly as sent (trimmed), so the client's chosen glyphs round-trip; `UpdateMedia` fails with `INVALID_ARGUMENT` (`invalid_start_key`/`invalid_end_key`) for anything else. Unset (or blank) clears it. Validated identically by the Elm client (`Shared.MediaViewerPanel.keyError`) and the backend (`models::is_valid_musical_key`) -- keep them in sync. Seeded like the BPMs: from the file's key tag (ID3 `TKEY`/`INITIALKEY`, which sets both; spelled-out forms like `A minor` are normalized to `Am`; Camelot codes like `8A` are ignored), else estimated from the first/last 30s of the audio. Estimates use bare majors and `m` minors, flats for `Db`/`Eb`/`Ab`/`Bb` and sharps for `F#`/`C#m`/`G#m`/`F#m` (e.g. `Bb`, `F#m`), and often confuse relative major/minor (`C` vs `Am`).
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).
Used in: , , , , ,
The ID of the media item.
An optional title for the media item.
Indicates the media was generated by the server rather than uploaded manually by a user.
See `Media.sizes`.
An external URL to fetch the media from, in lieu of `/media/{id}`. See `Media.url`. If unset, clients fall back to `/media/{id}`.
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.
See `Media.visibility`. Included so a client holding only a `MediaReference` (e.g. the media viewer) can show/edit it and tell whether it's `LICENSED`.
Media is a special type and less customizable than "Features."
Used in:
Hide the Posts or Events tab from the user with this flag.
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.
Only `SERVER_PUBLIC` and `GLOBAL_PUBLIC` are valid. `GLOBAL_PUBLIC` is only valid if default_user_permissions contains `GLOBALLY_PUBLISH_[USERS|GROUPS|POSTS|EVENTS]` as appropriate.
Default media storage allocation for newly created users. Defaults to 15MB.
"Block CORS Anonymous Media Access". When set, `GET /media/{id}` responses to *unauthenticated* requests omit CORS `Access-Control-Allow-*` headers even for `GLOBAL_PUBLIC` media, so browsers block other sites' scripts from reading them (plain `<img>`/`<video>` embeds still work). Authenticated requests are unaffected.
Whether `LICENSED` media (see `Visibility.LICENSED`) is discoverable by *unauthenticated* users: listed by `GetMedia` and its preview/thumbnails served by `GET /media/{id}`. When unset (the default), `LICENSED` media is only visible to logged-in users -- like `SERVER_PUBLIC` -- plus its owner and admins. Either way, its full-quality bytes still require an active License.
Default is 5GB (applied whenever this is `0`, same read-time-fallback convention as `default_user_media_allocation_bytes` above). API/server-enforced limit for total of all Media usage (`server_media_usage_bytes` below) -- `CreateMedia` rejects an upload that would push the server over this cap the same way it already rejects one that would push a user over their own `User.media_storage_limit_bytes`. Editing this requires [`EDIT_SERVER_MEDIA_ALLOCATION`](#rellm-Permission) via [`ConfigureServer`](#grpc-api-ConfigureServer) -- see that permission's own doc.
(Read-only) Amount of storage used by user-stored Media, according to its sizing data. Adjusted (by a cheap incremental delta, not a full recompute) on CreateMedia calls, deletes, and size conversions, and also fully recomputed every 3 minutes by the `calculate_server_media_usage` background job (correcting any drift the incremental call sites missed).
When `server_media_usage_bytes` was last written -- by either an incremental adjustment or a full recompute (see that field's own doc); not limited to just the periodic job's runs.
(Read-only) Amount of storage used by S3 (or compatible) object storage. Periodically computed via list+sum from object storage itself, by the `calculate_server_object_storage_usage` background job (every 4h) -- this is a drift check against `server_media_usage_bytes` above (which is derived from the `media` table, not object storage itself), so unlike that field this is never incrementally adjusted between runs.
Turns off the `generate_link_preview_images` background job's generation of a preview image (the page's main image, or failing that a screenshot of it) for posts with links. Off by default (i.e. generation is *on*), including for servers configured before this field existed. Doesn't affect previews that already exist -- see [`DeleteLinkPreviewImages`](#grpc-api-DeleteLinkPreviewImages) for that. Editable by anyone who can `ConfigureServer` (`ADMIN`).
Whether link preview generation first tries the page's own metadata image (`og:image`/`twitter:image`), found by simply fetching the page's HTML -- no browser needed, so it's much cheaper -- and only falls back to loading the page in a browser if there isn't one (or it's under 1KB, e.g. a tracking pixel). Off by default, in which case every link goes straight to the browser: its main-image detection (the largest visible content image, else the metadata image), else a screenshot. Either way a post gets exactly one generated preview. Metadata images are what the page *declares* as its preview, which for some sites is a generic site-wide logo rather than anything about the linked page -- hence opt-in. Has no effect while `disable_link_preview_images` is set, nor on existing previews (see [`DeleteLinkPreviewImages`](#grpc-api-DeleteLinkPreviewImages) to regenerate those).
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.
Used in: ,
Which copy this is -- the untouched original, or one of the auto-generated resized copies.
This copy's size on disk/in object storage, in bytes. Summed (across every size, of every Media a user owns) into `User.media_storage_bytes_used`.
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.
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.
`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 `oneof`s stay independent Rust types (see `logic::market::market_fulfillment::terminate_entitlement`'s own doc for why that distinction matters for `PermissionsAccessPurchaseDetails`/`PermissionsAccessSubscriptionDetails`).
Used in:
The buyer's new total media storage allocation, replacing (not adding to) whatever quota they already had -- see `logic::market::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.
Used in: ,
How much media storage this product/subscription grants the buyer, replacing (not adding to) whatever quota they already had -- see `logic::market::market_fulfillment::fulfill_purchase`'s `MediaStorage` arm.
Used when fetching group members using the [`GetMembers`](#grpc-api-GetMembers) RPC.
Used in:
The user.
The user's membership (or join request, or invitation, or both) in the group.
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.
Used as request type in: Rellm.CreateMembership, Rellm.DeleteMembership, Rellm.UpdateMembership
Used as response type in: Rellm.CreateMembership, Rellm.UpdateMembership
Used as field type in: , ,
The member (or requested/invited member).
The group the membership pertains to.
Valid Membership Permissions are: `VIEW_POSTS`, `CREATE_POSTS`, `MODERATE_POSTS`, `VIEW_EVENTS`, CREATE_EVENTS, `MODERATE_EVENTS`, `ADMIN`, `RUN_BOTS`, and `MODERATE_USERS`
Tracks whether group moderators need to approve the membership.
Tracks whether the user needs to approve the membership.
The time the membership was created.
The time the membership was last updated.
A Rellm `Message` represents a single message/email sent to one or more recipients (really, "zero or more", as the design incorporates undeliverable messages).
Used as response type in: Rellm.SendMessage
Used as field type in:
The ID of the message.
The sender of the message. Note that this is *purported* (we don't protect against spoofing).
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.
The body text of the message. For email messages, this is the email body.
Subject of the message. For email messages, this is the email subject.
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.
If this message derived from an email, the original email's "from" address.
If this message derived from an email, the original email's "to" address.
If this message derived from an email, the original email's "cc" address.
If this message derived from an email, the original email's "bcc" address.
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`](#grpc-api-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.
The time the message was created.
Used in:
Gets messages sent to the current user, and messages (purportedly) sent by the user.
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.
Gets all messages on the server (to a limit), including those sent to other users. Requires admin privileges.
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.
Used in: ,
When the message was marked read. Always set on a [`MessageRead`](#rellm-MessageRead) returned from [`MarkMessagesRead`](#grpc-api-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`.
Used in:
The ID of the messaging group.
The users who are members of the group. Note that this is a superset of the users who are
The time the group was created.
Nearly everything in Rellm has one or more `Moderation`s on it. From a high level: - A [`User`](#rellm-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!) - This is managed by `people_settings.default_moderation` in [`ServerConfiguration`](#rellm-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. - A [`Follow`](#rellm-Follow) has a `target_user_moderation` that determines whether the [`User`](#rellm-User) is following the [`Group`](#rellm-Group). - It is managed by `default_follow_moderation` in the targeted [`User`](#rellm-User). - A [`Group`](#rellm-Group) has a `moderation` that determines whether the [`Group`](#rellm-Group) is visible to users (per its `visibility`). - This is managed by `group_settings.default_moderation` in [`ServerConfiguration`](#rellm-ServerConfiguration). - A [`Membership`](#rellm-Membership) has a `group_moderation` and `user_moderation` that determine whether the [`Group`](#rellm-Group) admins and/or the invited user has approved the [`Membership`](#rellm-Membership), respectively. - User invites to [`Group`](#rellm-Group)s (i.e. the `user_moderation`) always start as `PENDING`. The group side of this is managed by `default_membership_moderation` of the [`Group`](#rellm-Group) in question. - A [`Post`](#rellm-Post) has a `moderation` that determines whether the [`Post`](#rellm-Post) is visible to users (per its `visibility`). - This is managed by `post_settings.default_moderation` in [`ServerConfiguration`](#rellm-ServerConfiguration). - A [`GroupPost`](#rellm-GroupPost) has a `moderation` that determines whether the admins/mods of the [`Group`](#rellm-Group) has approved the [`Post`](#rellm-Post) (or [`Post`](#rellm-Post)-descended thing like [`Event`](#rellm-Event)s). - [`Event`](#rellm-Event)s and further objects contain a [`Post`](#rellm-Post) and thus inherit its `moderation` and related [`GroupPost`](#rellm-GroupPost) behavior, for "Group Events."
Used in: , , , , , , , , , , , , ,
A moderation that is not known to the protocol. (Likely, the client and server use different versions of the Rellm protocol.)
Subject has not been moderated and is visible to all users.
Subject is awaiting moderation and not visible to any users.
Subject has been approved by moderators and is visible to all users.
Subject has been rejected by moderators and is not visible to any users.
The default navigation tabs in Rellm's Elm UI.
Used in: ,
The home/landing tab.
The Events tab.
The Posts tab.
The People tab.
The About tab.
The Market tab.
The Media tab: Video, Audio, Images and (when logged in) My Media sub-tabs. Reserved path: `media`.
The Video tab (a YouTube-alike over `GetMedia` with `content_type: "video/*"`). Reserved paths: `video`, `videos`.
The Audio tab (a Spotify-alike over `GetMedia` with `content_type: "audio/*"`). Reserved path: `audio`.
The Images tab (a photo-gallery view over `GetMedia` with `content_type: "image/*"`). Reserved path: `images`.
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.
Used in:
Just the icon/emoji, no visible title (Rellm's original, still-default look).
Just the title text, no visible icon.
Icon above, title below, stacked in one tab.
Icon and title side by side, icon first.
The time-based component of an [`Event`](#rellm-Event). Has a `starts_at` and `ends_at` time, a [`Location`](#rellm-Location), and an optional [`Post`](#rellm-Post) (and discussion thread) specific to this particular `Occasion` in addition to the parent [`Event`](#rellm-Event).
Used as response type in: Rellm.SyncOccasion
Used as field type in:
ID of the parent [`Event`](#rellm-Event) (i.e. the parent `Event.post.id`).
Optional [`Post`](#rellm-Post) containing alternate title/link/description for this particular Occasion. Its [`PostContext`](#rellm-PostContext) should be `OCCASION`. An `Occasion`'s ID *is* its `post.id` - there is no separate surrogate ID.
Additional configuration for this [`Occasion`](#rellm-Occasion) beyond the [`EventInfo`](#rellm-EventInfo) in its parent [`Event`](#rellm-Event).
The time the event starts (UTC/Timestamp format).
The time the event ends (UTC/Timestamp format).
The location of the event.
The time since this event "disappeared" from the sync source. It is up to the owner whether this means it should be deleted.
RSVP + invite data for this Occasion.
If the request was made by a logged-in user, this is the current user's RSVP for this Occasion.
SyncDestinations this Occasion has been synced (cross-posted) to, and their status.
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.
Used in:
RSVP configuration and metadata for the Occasion.
Consolidated type for RSVP info for an [`Occasion`](#rellm-Occasion). Curently, the `optional` counts below are *never* returned by the API.
Used in:
Overrides `EventInfo.allows_rsvps`, if set, for this Occasion.
Overrides `EventInfo.allows_anonymous_rsvps`, if set, for this Occasion.
Overrides `EventInfo.max_attendees`, if set, for this Occasion. Enforced the same way - see there.
The number of users who have RSVP'd to the event.
The number of attendees who have RSVP'd to the event. (RSVPs may have multiple attendees, i.e. guests.)
The number of users who have signaled interest in the event.
The number of attendees who have signaled interest in the event. (RSVPs may have multiple attendees, i.e. guests.)
The number of users who have been invited to the event.
The number of attendees who have been invited to the event. (RSVPs may have multiple attendees, i.e. guests.)
Credentials for an [OpenAI API](https://platform.openai.com/docs/api-reference) connection, accepted by [`CreateAIProvider`](#grpc-api-CreateAIProvider)/[`UpdateAIProvider`](#grpc-api-UpdateAIProvider). Used for image generation/editing via OpenAI's [Images API](https://platform.openai.com/docs/guides/image-generation) (the GPT Image model family) - same use case as [`GeminiCredentials`](#rellm-GeminiCredentials), see [`GenerateMedia`](#grpc-api-GenerateMedia).
Used in:
The OpenAI API key. Required (and only used) on [`CreateAIProvider`](#grpc-api-CreateAIProvider)/[`UpdateAIProvider`](#grpc-api-UpdateAIProvider) -- never populated in responses (see [`GeminiCredentials.gemini_api_key`](#rellm-GeminiCredentials)).
Rellm Permissions are a set of permissions that can be granted directly to [`User`](#rellm-User)s and [`Membership`](#rellm-Membership)s. (A [`Membership`](#rellm-Membership) is the link between a [`Group`](#rellm-Group) and a [`User`](#rellm-User).) Subsets of these permissions are also applicable to anonymous users via [`anonymous_user_permissions` in `ServerConfiguration`](#rellm-ServerConfiguration), and to Group non-members via [`non_member_permissions` in `Group`](#rellm-Group), as well as others documented there.
Used in: , , , , , , ,
A permission that could not be read using the Rellm protocol. (Perhaps, a permission from a newer Rellm version.)
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).
Allow the user to publish profiles with `SERVER_PUBLIC` Visibility. This generally only applies to the user's own profile, except for Admins.
Allow the user to publish profiles with `GLOBAL_PUBLIC` Visibility. This generally only applies to the user's own profile, except for Admins.
Allow the user to grant `VIEW_POSTS`, `CREATE_POSTS`, `VIEW_EVENTS` and `CREATE_EVENTS` permissions to users.
Allow the user to follow other users.
Allow the user to grant Basic Permissions to other users. "Basic Permissions" are defined by your [`ServerConfiguration`](#rellm-ServerConfiguration)'s `basic_user_permissions`.
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).
Allow the user to create groups.
Allow the user to give groups `SERVER_PUBLIC` visibility.
Allow the user to give groups `GLOBAL_PUBLIC` visibility.
The Moderate Groups permission makes a user effectively an admin of *any* group.
Allow the user to (potentially request to) join groups of `SERVER_PUBLIC` or higher visibility.
Allow the user to invite other users to groups. Only applicable as a Group permission (not at the User level).
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 [`GroupPost`](#rellm-GroupPost)s whose [`Post`](#rellm-Post)s have `LIMITED` or higher visibility. Allow anonymous users to view posts with `GLOBAL_PUBLIC` visibility (when configured as an anonymous user permission).
As a user permission, allow the user to create [`Post`](#rellm-Post)s of `PRIVATE` and `LIMITED` visibility. As a group permission, allow the user to create [`GroupPost`](#rellm-GroupPost)s for `POST` and `FEDERATED_POST` [`PostContext`](#rellm-PostContext)s..
Allow the user to publish posts with `SERVER_PUBLIC` visibility.
Allow the user to publish posts with `GLOBAL_PUBLIC` visibility.
Allow the user to moderate posts.
Allow the user to reply to posts.
Allow the user to edit post titles and/or links.
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 [`GroupPost`](#rellm-GroupPost)s whose [`Event`](#rellm-Event) [`Post`](#rellm-Post)s have `LIMITED` or higher visibility. Allow anonymous users to view events with `GLOBAL_PUBLIC` visibility (when configured as an anonymous user permission).
As a user permission, allow the user to create [`Event`](#rellm-Event)s of `PRIVATE` and `LIMITED` visibility. As a group permission, allow the user to create [`GroupPost`](#rellm-GroupPost)s for `EVENT` and `FEDERATED_OCCASION` [`PostContext`](#rellm-PostContext)s..
Allow the user to publish events with `SERVER_PUBLIC` visibility.
Allow the user to publish events with `GLOBAL_PUBLIC` visibility.
Allow the user to moderate events.
Allow the user to RSVP to events that allow RSVPs.
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.*
Allow the user to create media of `PRIVATE` and `LIMITED` visibility. *Not currently enforced.*
Allow the user to publish media with `SERVER_PUBLIC` visibility. *Not currently enforced.*
Allow the user to publish media with `GLOBAL_PUBLIC` visibility. *Not currently enforced.*
Allow the user to moderate events.
Allow the user to create/update their own [`AIProvider`](#rellm-AIProvider)s (see `ai_providers.proto`) and grant/revoke other users' access to them.
Allow the user to create/update [`SyncSource`](#rellm-SyncSource)s (iCal subscriptions) that synchronize [`Event`](#rellm-Event)s in.
Allow the user to create/update [`SyncSource`](#rellm-SyncSource)s (RSS subscriptions) that synchronize [`Post`](#rellm-Post)s in.
Allow the user to create/update [`SyncSource`](#rellm-SyncSource)s (Atom subscriptions) that synchronize [`Post`](#rellm-Post)s in.
Allow the user to create/update [`SyncDestination`](#rellm-SyncDestination)s that cross-post Occasions to a connected Facebook Page, and to sync Occasions to them.
Allow the user to create/update [`SyncDestination`](#rellm-SyncDestination)s that cross-post Posts to a connected Facebook Page, and to sync Posts to them.
Allow the user to create/update [`SyncDestination`](#rellm-SyncDestination)s that cross-post Occasions to a connected Instagram Business/Creator account, and to sync Occasions to them.
Allow the user to create/update [`SyncDestination`](#rellm-SyncDestination)s that cross-post Posts to a connected Instagram Business/Creator account, and to sync Posts to them.
Allow the user to create/update [`SyncDestination`](#rellm-SyncDestination)s that cross-post Occasions to a connected Mastodon account, and to sync Occasions to them.
Allow the user to create/update [`SyncDestination`](#rellm-SyncDestination)s that cross-post Posts to a connected Mastodon account, and to sync Posts to them.
Allow the user to create/update [`SyncDestination`](#rellm-SyncDestination)s that cross-post Occasions to a connected Bluesky account, and to sync Occasions to them.
Allow the user to create/update [`SyncDestination`](#rellm-SyncDestination)s that cross-post Posts to a connected Bluesky account, and to sync Posts to them.
Allow the user to create/update [`SyncDestination`](#rellm-SyncDestination)s that cross-post Occasions to a connected X (Twitter) account, and to sync Occasions to them.
Allow the user to create/update [`SyncDestination`](#rellm-SyncDestination)s that cross-post Posts to a connected X (Twitter) account, and to sync Posts to them.
Allow the user to create/update [`SyncDestination`](#rellm-SyncDestination)s that cross-post Occasions to a connected Threads account, and to sync Occasions to them.
Allow the user to create/update [`SyncDestination`](#rellm-SyncDestination)s that cross-post Posts to a connected Threads account, and to sync Posts to them.
Indicates the user is a business. Used purely for display purposes.
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.
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`](#rellm-User), [`Group`](#rellm-Group), [`Post`](#rellm-Post) or [`Event`](#rellm-Event). In the context of group permissions, allows the user to configure the group, modify members and member permissions, and moderate [`GroupPost`](#rellm-GroupPost)s and `GroupEvent`s.
Allow the user to view the private contact methods of other users. Kept separate from `ADMIN` to allow for more fine-grained privacy control.
Allow the user to edit [`ServerConfiguration.cluster_resources`](#rellm-ClusterResources) via [`ConfigureServer`](#grpc-api-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`](#grpc-api-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.
Allow the user to edit [`MediaSettings.server_media_allocation_bytes`](#rellm-MediaSettings) via [`ConfigureServer`](#grpc-api-ConfigureServer). Raising this cap changes how much total Media storage the server will accept across every user, so - same reasoning as `EDIT_CLUSTER_SETTINGS` above - it's kept separate from `ADMIN` and deliberately *not* grantable via [`UpdateUser`](#grpc-api-UpdateUser), only settable directly in the database (e.g. via the `set_permission` binary).
Allow the user to read the server's recent logs via [`GetServerLogs`](#grpc-api-GetServerLogs). Server logs can contain other users' usernames, IP addresses and request details, and error output (panics, failed connections) that can echo server configuration. So - same reasoning as `EDIT_CLUSTER_SETTINGS` and `EDIT_SERVER_MEDIA_ALLOCATION` above - this is kept separate from `ADMIN` and `ADMIN` does **not** imply it: `GetServerLogs` requires this permission *exactly*. It is also deliberately *not* grantable via [`UpdateUser`](#grpc-api-UpdateUser) - an `UpdateUser` request can neither grant nor revoke it, it is always carried forward from what the user already has - and is only settable directly in the database (e.g. via the `set_permission` binary), so granting it is always a deliberate operator action. Clients should recognize and display it (e.g. on a user's profile) but must **not** offer it in "grant permission" pickers.
`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::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.
Used in:
The permissions this purchase granted -- see `logic::market::market_fulfillment::fulfill_purchase`'s `PermissionsAccess` arm (adds these to the buyer's `User.permissions`, union-style).
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.
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::market_fulfillment::terminate_entitlement` tell "what to claw back" apart from "what was originally billed").
Used in: ,
Which `Permission`s this product/subscription grants the buyer -- see `logic::market::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.
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.
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.
A `Post` is a message that can be posted to the server. Its `visibility` as well as any associated [`GroupPost`](#rellm-GroupPost)s and [`UserPost`](#rellm-UserPost)s determine what users see it and where. `Post`s 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.
Used as request type in: Rellm.CreatePost, Rellm.DeletePost, Rellm.StarPost, Rellm.StreamReplies, Rellm.UnstarPost, Rellm.UpdatePost
Used as response type in: Rellm.CreatePost, Rellm.DeletePost, Rellm.StarPost, Rellm.StreamReplies, Rellm.SyncPost, Rellm.UnstarPost, Rellm.UpdatePost
Used as field type in: , ,
Unique ID of the post.
The author of the post. This is a smaller version of User.
If this is a reply, this is the ID of the post it's replying to.
The title of the post. This is invalid for replies.
The link of the post. This is invalid for replies.
The content of the post. This is required for replies.
The number of responses (replies *and* replies to replies, etc.) to this post.
The number of *direct* replies to this post.
The number of groups this post is in.
List of Media IDs associated with this post. Order is preserved.
Flag indicating whether Media has been generated for this Post. Currently previews are generated for any Link post.
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 of the Post (`POST`, `REPLY`, `EVENT`, or `OCCASION`.)
The visibility of the Post.
The moderation of the Post.
The desired end-user layout of Media attached to the post.
If the Post was retrieved from GetPosts with a group_id, the GroupPost metadata may be returned along with the Post.
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.
The time the post was created.
The time the post was last updated.
The time the post was published (its visibility first changed to `SERVER_PUBLIC` or `GLOBAL_PUBLIC`).
The time the post was last interacted with (replied to, etc.)
The number of unauthenticated stars on the post.
SyncDestinations this post has been synced (cross-posted) to, and their status.
If the Post was created/is kept in sync from a [`SyncSource`](#rellm-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.
A post this post quotes (embeds), for posts translated from a federated network such as Bluesky or Mastodon. Only ever set client-side by the Elm frontend's federation translators -- Rellm servers never populate it. May itself quote another post, up to however many levels the client chose to resolve. A Rellm post quotes another post by putting a link to it in its own `link` -- clients embed it when `link` is (or points at) a `/post/{id}`, `/event/{id}` or short `/{x}{id}` URL on any Rellm server, or a Bluesky or Mastodon post (its own URL, or a Rellm `/post/{id}@bluesky:` / `@mastodon:{host}` route). That's only meaningful on a `POST`-context post (`link` is invalid for replies).
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.
Used in: ,
"Standard" or "Top-Level" Post. Can have media, a link, a title, and/or content. If provided, its `link` and `title` are permanent.
Reply to a `POST`, `REPLY`, `EVENT`, or `OCCASION` Does not support a `link`. Requires a `reply_to_post_id`.
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.
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.
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.
Used in:
Gets SERVER_PUBLIC and GLOBAL_PUBLIC posts as is sensible. Also usable for getting replies anywhere.
Returns posts from users the user is following.
Returns posts from any group the user is a member of.
Returns `DIRECT` posts that are directly addressed to the user.
Returns posts pending moderation by the server-level mods/admins.
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.
Returns posts published recently (within the last 14 days, or, if `published_or_created_before` is given, the 14 days before that time), scoped the same way ALL_ACCESSIBLE_POSTS is (plus author_user_id, if provided), ranked by popularity rather than strictly by recency. A post's score is `(unauthenticated_star_count + 2 * response_count + 1) / (age_in_hours + 2) ^ 1.5` - engagement (stars, and responses to the post, which count double) decayed by how long ago it was published (`published_at`, falling back to `created_at`), like Hacker News' ranking. The `+ 1` means a post with no engagement still scores, just lower, and ties fall to recency - so on a quiet server this degrades gracefully to roughly ALL_ACCESSIBLE_POSTS. Like every listing, returns at most one page of results; there is no further paging (the ranking drifts as time passes, so offsets aren't stable) - narrow by `published_or_created_before` instead.
Returns posts from a specific group. Requires group_id parameter.
Returns pending_moderation posts from a specific group. Requires group_id parameter and user must have group (or server) admin permissions.
Used in:
Specific settings for Posts.
Used in:
Hide the Posts tab from the user with this flag.
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.
Only `SERVER_PUBLIC` and `GLOBAL_PUBLIC` are valid. `GLOBAL_PUBLIC` is only valid if default_user_permissions contains `GLOBALLY_PUBLISH_[USERS|GROUPS|POSTS|EVENTS]` as appropriate.
Can be used to rename, e.g., "Post" "Highlight" or "Squirt"
Can be used to rename, e.g. "Posts" to "Splurts" or "Memories"
Controls whether replies are shown in the UI. Note that users' ability to reply is controlled by the `REPLY_TO_POSTS` permission.
Strategy when a user sets their visibility to `PRIVATE`.
Used in:
`PRIVATE` Users can't see other Users (only `PUBLIC_GLOBAL` Visilibity Users/Posts/Events). Other users can't see them.
Users can see other users they follow, but only `PUBLIC_GLOBAL` Visilibity Posts/Events. Other users can't see them.
Users can see other users they follow, including their `PUBLIC_SERVER` Posts/Events. Other users can't see them.
How often a `MarketProduct`/`MarketSubscription` bills. Immutable on a `MarketProduct` once created (see that message's own doc) -- same reasoning as `PurchaseType`'s immutability.
Used in: ,
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).
Renews (re-bills and re-fulfills) once per year, via `renew_market_subscriptions.rs`.
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::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.
Used in: , ,
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_user_media_allocation_bytes`), not to unlimited.
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.
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).
A bundle of `Permission`s (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::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).
Metadata on a refresh token for the current user, used when managing refresh tokens as a user. Does not include the token itself.
Used in:
The DB ID of the refresh token. Used when deleting the token or updating the device_name.
Expiration date of the refresh token.
The device name the refresh token is on. User-updateable.
Whether the refresh token is associated with the current device (based on what user is making the request).
Returned when creating an account, logging in, or creating a third-party refresh token.
Used as response type in: Rellm.CreateAccount, Rellm.Login
The persisted token the device should store and associate with the account. Used to request new access tokens.
An initial access token provided for convenience.
The user associated with the account that was created/logged into.
`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.
Used in: ,
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.
Same caveat as `db_size_bytes` above -- currently always `0`. Intended to be the requested object storage size in bytes.
Copied from `RellmHostingSubscriptionDetails.additional_description` at the moment this purchase was fulfilled -- see that field's own doc.
The domain the buyer wants their new Rellm instance reachable at (e.g. "myserver.example.com").
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).
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.
Used in: ,
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.
Same caveat as `db_size_bytes` above. On a `MarketProduct`: the object storage size (in bytes) this product is configured to provision.
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.
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`.
On a `MarketProduct`: unset/meaningless. On a `MarketSubscription`: where the fulfilling admin should reach the buyer about this order, from `RellmHostingPurchaseDetails.contact_email`.
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.
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::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.
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).
An RSVP: describes a user's attendance at an [`Occasion`](#rellm-Occasion). Such as: * A user's RSVP to an [`Occasion`](#rellm-Occasion) (one of `INTERESTED`, `GOING`, `NOT_GOING`, or , `REQUESTED` (i.e. invited)). * Invitation status of a user to an [`Occasion`](#rellm-Occasion). * [`ContactMethod`](#rellm-ContactMethod)-driven management for anonymous RSVPs to an [`Occasion`](#rellm-Occasion).
Used as request type in: Rellm.DeleteRsvp, Rellm.UpsertRsvp
Used as response type in: Rellm.UpsertRsvp
Used as field type in: ,
Unique server-generated ID for the RSVP.
ID of the [`Occasion`](#rellm-Occasion) the RSVP is for.
If the RSVP is non-anonymous, core data about the user.
If the RSVP is anonymous, core data about the anonymous attendee.
Number of guests including the RSVPing user. (Minimum 1).
The user's RSVP to an [`Occasion`](#rellm-Occasion) (one of `INTERESTED`, `REQUESTED` (i.e. invited), `GOING`, `NOT_GOING`)
User who invited the attendee. (Not yet used.)
Public note for everyone who can see the event to see.
Private note for the event owner.
Moderation status for the RSVP. Moderated by the [`Event`](#rellm-Event) owner (or [`Occasion`](#rellm-Occasion) owner if applicable).
The time the RSVP was created.
The time the RSVP was last updated.
Occasion RSVP statuses. State transitions may generally happen in any direction, but: * `REQUESTED` can only be selected if another user invited the user whose RSVP 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.
Used in: ,
The user is (or was) interested in attending. This is the default status.
Another user has invited the user to the event.
The user plans to go to the event, or went to the event.
The user does not plan to go to the event, or did not go to the event.
Response to get RSVP data for an event.
Used as response type in: Rellm.GetRsvps
Used as field type in:
The RSVP data for the event, in no particular order.
When `hide_location_until_rsvp_approved` is set, the location of the event.
Totals over *every* RSVP the viewer is allowed to see for the Occasion -- not just the ones returned in `rsvps`. `GetEvents` caps how many `rsvps` it returns per Occasion (for big events), so these counts can be *greater than* `rsvps.length` -- much like `Post.reply_count` can exceed the replies actually loaded. (`GetRsvps` is not capped, so there they match `rsvps` exactly.) `*_count` is a number of RSVPs; `*_attendees` is the sum of their `number_of_guests`. The per-status counts only include RSVPs whose moderation passes (`UNMODERATED`/`APPROVED`) -- the ones everyone can see. `pending_*` counts the `PENDING` ones the viewer can see (their own, or all of them for the event owner), whatever their status. `REJECTED` RSVPs are not counted.
Color in ARGB hex format (i.e `0xAARRGGBB`).
Used in:
App Bar/primary accent color.
Nav/secondary accent color.
Color used on author of a post in discussion threads for it.
Color used on author for admin posts.
Color used on author for moderator posts.
Configuration for a Rellm server instance.
Used as request type in: Rellm.ConfigureServer
Used as response type in: Rellm.ConfigureServer, Rellm.GetServerConfiguration
The name, description, logo, color scheme, etc. of the server.
The federation configuration for the server.
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 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`.
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`.
Configuration for users on the server. If default visibility is `GLOBAL_PUBLIC`, default_user_permissions *must* contain `PUBLISH_USERS_GLOBALLY`.
Configuration for groups on the server. If default visibility is `GLOBAL_PUBLIC`, default_user_permissions *must* contain `PUBLISH_GROUPS_GLOBALLY`.
Configuration for posts on the server. If default visibility is `GLOBAL_PUBLIC`, default_user_permissions *must* contain `PUBLISH_POSTS_GLOBALLY`.
Configuration for events on the server. If default visibility is `GLOBAL_PUBLIC`, default_user_permissions *must* contain `PUBLISH_EVENTS_GLOBALLY`.
Configuration for media on the server. If default visibility is `GLOBAL_PUBLIC`, default_user_permissions *must* contain `PUBLISH_MEDIA_GLOBALLY`.
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.
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-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`](#grpc-api-GetServerConfiguration) for non-admins/anonymous callers); editing it via [`ConfigureServer`](#grpc-api-ConfigureServer) additionally requires the [`EDIT_CLUSTER_SETTINGS`](#rellm-Permission) permission.
Strategy when a user sets their visibility to `PRIVATE`. Defaults to `ACCOUNT_IS_FROZEN`.
(TODO) Allows admins to enable/disable creating accounts and logging in. Eventually, external auth too hopefully!
Web Push (VAPID) configuration for the server.
Which [`ContactProtocol`](#rellm-ContactProtocol)s (`tel:`/`mailto:`) this server currently accepts -- drives [`ContactMethod.supported_by_server`](#rellm-ContactMethod) (`users.proto`, which can't reference this message directly -- see that field's own doc for why). Settable via [`ConfigureServer`](#grpc-api-ConfigureServer), which *errors* rather than silently dropping an invalid entry: `CONTACT_PROTOCOL_TEL` requires an enabled [`TwilioConfig`](#rellm-TwilioConfig)/[`BirdConfig`](#rellm-BirdConfig)/ [`TelnyxConfig`](#rellm-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`.
A server-preferred order of [`ContactVerificationAPI`](#rellm-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`](#rellm-ServerConfiguration) below. Even when this is blank, an enabled provider is still tried (in the fixed default order Telnyx/Bird/Twilio) -- this field only matters when more than one is enabled and the admin wants a specific one tried first. Only serialized for admin users.
Derived from whether [`TwilioConfig`](#rellm-TwilioConfig).twilio_enabled/ [`BirdConfig`](#rellm-BirdConfig).bird_enabled/ [`TelnyxConfig`](#rellm-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 -- see [`TwilioConfig`](#rellm-TwilioConfig). Only serialized for admin users.
Bird (bird.com, formerly MessageBird) Config -- a cheaper alternative to [`TwilioConfig`](#rellm-TwilioConfig) for SMS verification; see [`BirdConfig`](#rellm-BirdConfig)'s own doc. Only serialized for admin users.
Stripe Config, backing the Marketplace (`market.proto`). Only serialized for admin users.
Telnyx Config -- another alternative SMS verification provider to [`TwilioConfig`](#rellm-TwilioConfig) (see [`TelnyxConfig`](#rellm-TelnyxConfig)'s own doc). Only serialized for admin users.
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.
The server's [`CustomCSSConfiguration`](#rellm-CustomCSSConfiguration): which media its custom stylesheet uses and whether it forces the light or dark theme. Publicly readable. `custom_css` itself is always unset here -- fetch it with [`GetCustomCSS`](#grpc-api-GetCustomCSS), which is the only way to read the (potentially large) stylesheet text. Ignored by [`ConfigureServer`](#grpc-api-ConfigureServer); write it with [`ConfigureCustomCSS`](#grpc-api-ConfigureCustomCSS).
User-facing information about the server displayed on the "about" page.
Used in:
Name of the server.
Short name of the server. Used in URLs, etc. (Currently unused.)
Description of the server.
The server's privacy policy. Will be displayed during account creation and on the `/about` page.
Multi-size logo data for the server.
The web UI to use (React/Tamagui (default) vs. Flutter Web)
The color scheme for the server.
The media policy for the server. Will be displayed during account creation and on the `/about` page.
If set, the Elm SPA's "Countabilibar" -- the row under the breadcrumbs counting the posts and events a page shows per server/user, with checkboxes to hide them -- is not shown. (Field 9 was formerly `repeated string recommended_server_hosts`, replaced by `FederationInfo.servers`; stored configurations may still carry that old value, which is ignored.)
Logo data for the server. Built atop Rellm [`Media` APIs](#rellm-Media).
Used in:
The media ID for the square logo.
The media ID for the square logo in dark mode.
The media ID for the wide logo.
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.
Used in:
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`).
Used in:
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 (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.
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.
A user-owned destination to sync (cross-post) content out to. Mirrors [`SyncSource`](#rellm-SyncSource), but for pushing content out rather than pulling content in. Originally Event-specific (as `EventSyncDestination`), now shared by both [`Occasion`](#rellm-Occasion)s (see `events.proto`'s [`SyncOccasionRequest`](#rellm-SyncOccasionRequest)) and [`Post`](#rellm-Post)s (see `posts.proto`'s [`SyncPostRequest`](#rellm-SyncPostRequest)).
Used as request type in: Rellm.CreateSyncDestination, Rellm.UpdateSyncDestination
Used as response type in: Rellm.CreateSyncDestination, Rellm.UpdateSyncDestination
Used as field type in: , ,
Unique ID for the destination.
The user information for the owner of this destination.
The time the SyncDestination was created.
The time the SyncDestination was last updated.
The number of Occasions synced to this destination so far. Computed with a `COUNT` at request time (unlike [`SyncSource`](#rellm-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.
The number of Posts synced to this destination so far. Computed the same way as `synced_occasion_count`, just against Posts instead of Occasions.
A connected Facebook Page to post Occasions/Posts to.
A connected Instagram Business/Creator account to post Occasions/Posts to.
A connected Mastodon account to post Occasions/Posts to.
A connected Bluesky account to post Occasions/Posts to.
A connected X (Twitter) account to post Occasions/Posts to.
A connected Threads account to post Occasions/Posts to.
The status of a single piece of content's (an [`Occasion`](#rellm-Occasion) or [`Post`](#rellm-Post)) sync (cross-post) to one [`SyncDestination`](#rellm-SyncDestination). Shared/generic so both `Occasion.sync_destinations` and `Post.sync_destinations` can reuse it.
Used in: ,
The SyncDestination this status is for.
The ID of the resulting post on the destination (e.g. a Facebook Post ID).
A link to the resulting post on the destination, if available.
The time this content was last successfully synced to the destination.
A user-owned source to sync events from.
Used as request type in: Rellm.CreateSyncSource, Rellm.UpdateSyncSource
Used as response type in: Rellm.CreateSyncSource, Rellm.UpdateSyncSource
Used as field type in: , , ,
Unique ID for the synchronization.
The user information for the owner of this sync source.
How frequently the sync should happen in seconds.
The time the SyncSource was created.
The time the SyncSource was last updated.
The time the SyncSource was last synced.
The number of events total associated with this SyncSource. Recomputed on each sync.
The number of occasions total associated with this SyncSource. Recomputed on each sync.
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 or StellarWP source); always 0 for an ICS/StellarWP source, which syncs Events/Occasions instead.
The `User-Agent` this source's fetches send. Unset on create means `NO_USER_AGENT`; unset on update leaves the current value alone. Always set in responses.
The iCal subscription URL for the calendar sync. Creates/updates Events/Occasions.
The iCal export URL of a site running [The Events Calendar](https://theeventscalendar.com/) (a StellarWP WordPress plugin; the URL is the plugin's "Export" link, e.g. `https://example.com/?post_type=tribe_events&ical=1&eventDisplay=list`). Creates/updates Events/Occasions, like `ics_subscription_url`, but each sync follows the plugin's `paged=N` pagination to fetch as many pages as it can -- the plugin caps a single iCal response at its "events per page" setting (30 on many sites), so a plain `ics_subscription_url` only ever sees the next couple of weeks of events on a busy calendar. The plugin's list only shows *upcoming* events, so unlike `ics_subscription_url` an event dropping out of the feed doesn't mean it was removed: events that have already ended are kept, not pruned.
The RSS subscription URL for the feed sync. Creates/updates plain Posts.
The Atom subscription URL for the feed sync. Creates/updates plain Posts.
The HTTP `User-Agent` a [`SyncSource`](#rellm-SyncSource) sends when fetching its feed. Some feeds sit behind bot protection that only admits certain clients (e.g. raleighnc.gov's Cloudflare rules let `curl/*` through and challenge everything else, including a request with no `User-Agent` at all).
Used in:
Send no `User-Agent` header at all. What every source did before this setting existed.
Identify as Rellm: `Rellm-SyncSource/1.0 (+https://rellm.org)`.
Identify as `curl/8.7.1`.
Telnyx (https://telnyx.com) Config -- an alternative SMS verification provider to [`TwilioConfig`](#rellm-TwilioConfig), with a simpler single-API-key auth model like [`BirdConfig`](#rellm-BirdConfig)'s. See `docs/contact_integrations.md` for full setup steps.
Used in:
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`](#rellm-TwilioConfig)/ [`BirdConfig.bird_access_key`](#rellm-BirdConfig).
The Telnyx-provisioned sending number for outbound verification SMS (E.164, e.g. a toll-free number). Not secret.
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'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 (always blanked to `""` once written) for the same "don't echo config back" treatment as every other credential here -- see `TwilioConfig.twilio_webhook_signing_key`'s own doc for the full reasoning, including why storing a key here doesn't by itself turn verification on.
Whether `telnyx_webhook_signing_key` above is actually used to verify inbound deliveries -- same "explicit opt-in, defaults to `false`" reasoning as [`TwilioConfig.use_twilio_webhook_signing_key`](#rellm-TwilioConfig).
A connected Threads account - **a genuinely personal account works fine here**, unlike [`FacebookPage`](#rellm-FacebookPage)/[`InstagramAccount`](#rellm-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`](#rellm-FacebookAuthConfig)) rather than a separately-registered app, so no separate auth config is needed. Unlike [`FacebookPage`](#rellm-FacebookPage)/[`InstagramAccount`](#rellm-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`](#rellm-InstagramAccount), a text-only post (no media at all) is valid.
Used in:
The account's Threads user ID, used for all posting calls.
The account's @username, populated by the server when the connection is made.
Only used (and required) on [`CreateSyncDestination`](#grpc-api-CreateSyncDestination): the OAuth authorization code from the Threads login popup. Never populated in responses.
Time filter that works on the `starts_at` and `ends_at` fields of [`Occasion`](#rellm-Occasion). API currently only supports `ends_after`.
Used in:
Filter to events that start after the given time.
Filter to events that end after the given time.
Filter to events that start before the given time.
Filter to events that end before the given time.
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`](#rellm-ContactMethod) (`users.proto`) for how a verified `tel:` contact method surfaces this provider.
Used in:
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.
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.
The Twilio API Key's Secret, used as the Basic Auth password. Never serialized once written.
The Twilio-provisioned sending number for outbound verification SMS. Not secret.
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. Write-only like every other credential here -- always blanked to `""` once written, so a client can never read the real value back. 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. Whether this key is actually *used* to verify inbound deliveries is controlled independently by `use_twilio_webhook_signing_key` below -- storing a key here doesn't by itself turn verification on.
Whether `twilio_webhook_signing_key` above is actually used to verify inbound `/contact_integrations/twilio/receive` deliveries. Defaults to `false` (unverified) even once a key is stored -- an admin must explicitly opt in, same "explicit switch, not implicit from presence" reasoning `ContactIntegrationsTab`'s own "Use Webhook Signing Key" toggle exists for. `false` while this is `false` means inbound deliveries are accepted without signature verification, same fail-safe-direction reasoning as before (see this message's own doc and `docs/contact_integrations.md`) -- the only effect an unauthenticated/spoofed delivery can have either way is *revoking* consent, never granting it.
Model for a Rellm user. This user may have [`Media`](#rellm-Media), [`Group`](#rellm-Group) [`Membership`](#rellm-Membership)s, [`Post`](#rellm-Post)s, [`Event`](#rellm-Event)s, and other objects associated with them.
Used as request type in: Rellm.DeleteUser, Rellm.GetAIProviders, Rellm.GetSyncDestinations, Rellm.GetSyncSources, Rellm.UpdateUser
Used as response type in: Rellm.GetCurrentUser, Rellm.UpdateUser
Used as field type in: , ,
Permanent string ID for the user. Will never contain a `@` symbol.
Impermanent string username for the user. Will never contain a `@` symbol.
The user's real name.
The user's email address.
The user's phone number.
The user's permissions. See [`Permission`](#rellm-Permission) for details.
The user's avatar. Note that its visibility is managed by the User and thus it may not be accessible to the current user.
The user's bio.
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.
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.
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.
The user's moderation status. See [`Moderation`](#rellm-Moderation) for details.
Only PENDING or UNMODERATED are valid.
The number of users following this user.
The number of users this user is following.
The number of users this user mutually follows (and is followed by).
The number of groups this user is a member of.
The number of posts this user has made.
The number of responses to [`Post`](#rellm-Post)s and [`Event`](#rellm-Event)s this user has made.
The number of events this user has created.
The number of occasions this user has created (across all of their events).
Presence indicates the current user is following or has a pending follow request for this user.
Presence indicates this user is following or has a pending follow request for the current user.
Returned by [`GetMembers`](#grpc-api-GetMembers) calls, for use when managing [`Group`](#rellm-Group) [`Membership`](#rellm-Membership)s. The [`Membership`](#rellm-Membership) should match the [`Group`](#rellm-Group) from the originating [`GetMembersRequest`](#rellm-GetMembersRequest), providing whether the user is a member of that [`Group`](#rellm-Group), has been invited, requested to join, etc..
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`
The target user's own linked SyncDestinations (e.g. Facebook Pages). Populated by [`GetUsers`](#grpc-api-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`](#grpc-api-Login)/[`CreateAccount`](#grpc-api-CreateAccount)/[`GetCurrentUser`](#grpc-api-GetCurrentUser) (always a self-view) - always empty otherwise, including via every other [`GetUsers`](#grpc-api-GetUsers) listing type.
The target user's own [`SyncSource`](#rellm-SyncSource)s. Unlike `sync_destinations`, also populated for the target user themselves *or an Admin* across every [`GetUsers`](#grpc-api-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`](#grpc-api-Login)/[`CreateAccount`](#grpc-api-CreateAccount)/[`GetCurrentUser`](#grpc-api-GetCurrentUser) (always a self-view). Always empty for any other viewer.
Every [`AIProvider`](#rellm-AIProvider) model the target user may currently call - their own providers' models, plus any models granted to them on other users' providers (see [`AIModel`](#rellm-AIModel)). Gated and populated the same way as `sync_sources` (target user themselves, or an Admin, across any [`GetUsers`](#grpc-api-GetUsers) listing type, plus [`Login`](#grpc-api-Login)/[`CreateAccount`](#grpc-api-CreateAccount)/[`GetCurrentUser`](#grpc-api-GetCurrentUser)).
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`](#grpc-api-GetUsers) listing type, plus [`Login`](#grpc-api-Login)/[`CreateAccount`](#grpc-api-CreateAccount)/[`GetCurrentUser`](#grpc-api-GetCurrentUser)).
The time the user was created.
The time the user was last updated.
Wire-identical to [Author](#rellm-Author), but with a different name to avoid confusion.
Used in:
The user ID of the attendee.
The username of the attendee.
The attendee's user avatar.
Ways of listing users.
Used in:
Get all users.
Get users the current user is following.
Get users who follow and are followed by the current user.
Get users who follow the current user.
Get users who have requested to follow the current user.
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`](#rellm-PostListingType)) because proto3 enum values share a single namespace across the whole `rellm` package (C++ scoping rules) - [`PostListingType`](#rellm-PostListingType) already claimed `TEXT_SEARCH`.
Scopes `TEXT_SEARCH` to users following `user_id`. Requires `search_text` and `user_id`.
Scopes `TEXT_SEARCH` to users `user_id` follows. Requires `search_text` and `user_id`.
Scopes `TEXT_SEARCH` to `user_id`'s friends (mutual follows). Requires `search_text` and `user_id`.
Scopes `TEXT_SEARCH` to the signed-in caller's pending follow requests. Requires `search_text`.
[TODO] Gets admins for a server.
A `UserPost` is a "direct share" of a [`Post`](#rellm-Post) to a [`User`](#rellm-User). Currently unused/unimplemented. See also: [`DIRECT` `Visibility`](#rellm-Visibility).
The ID of the user the post is shared with.
The ID of the post shared.
The time the post was shared.
Response for `GetUserRefreshTokens` RPC. Returns all refresh tokens associated with the current user.
The refresh tokens associated with the current user.
Visibility in Rellm is a complex topic. There are several different types of visibility, and each type of entity ([`User`](#rellm-User), [`Media`](#rellm-Media), [`Group`](#rellm-Group), then [`Post`](#rellm-Post)/[`Event`](#rellm-Event)/etc. with common logic) has different rules for visibility. From the top down, the rules break down as follows: - Even a `PRIVATE` entity is always visible to the user who owns it. - For [`Group`](#rellm-Group)s, this means all full members of the [`Group`](#rellm-Group). - For [`User`](#rellm-User)s, this is confusing and there is a whole [`PrivateUserStrategy`](#rellm-PrivateUserStrategy) thing in [`ServerConfiguration`](#rellm-ServerConfiguration) for this. - A `LIMITED` entity is visible to to the owner(s) and any explicitly associated [`User`](#rellm-User)s and [`Group`](#rellm-Group)s. Generally, this only applies to [`Post`](#rellm-Post)/[`Event`](#rellm-Event)/etc. entities. Associations exist via [`UserPost`](#rellm-UserPost)s and [`GroupPost`](#rellm-GroupPost)s. - This is currently only implemented for [`Group`](#rellm-Group)s and [`GroupPost`](#rellm-GroupPost)s. There are some choices to be made about how to implement this for [`User`](#rellm-User)s and [`UserPost`](#rellm-UserPost)s, and whether `DIRECT` should be a separate visibility type. - A `SERVER_PUBLIC` entity is visible to all authenticated users. - A `GLOBAL_PUBLIC` entity is visible to the open internet.
Used in: , , , , , , , , ,
A visibility that is not known to the protocol. (Likely, the client and server use different versions of the Rellm protocol.)
Subject is only visible to the user who owns it.
Subject is only visible to explictly associated Groups and Users. See: [`GroupPost`](#rellm-GroupPost) and [`UserPost`](#rellm-UserPost).
Subject is visible to all authenticated users.
Subject is visible to all users on the internet.
[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-UserPost).
Only applicable to [`Media`](#rellm-Media). The media's metadata is discoverable by logged-in users (like `SERVER_PUBLIC`) -- or by anyone, if `MediaSettings.licensed_media_visible_globally` is set -- but its full-quality bytes are only served to the owner, admins, and holders of an active [`License`](#rellm-License). Everyone else gets the `UNLICENSED_PREVIEW_MEDIUM` conversion.
Web Push (VAPID) configuration for the server.
Used in:
Public VAPID key for the server.
Private VAPID key for the server. *Never serialized to the client.* Admins: Edit this in the database's JSONB column directly.
Offers a choice of web UIs. Generally though, React/Tamagui is a century ahead of Flutter Web, so it's the default.
Used in:
Uses Flutter Web. Loaded from /app.
Uses Handlebars templates. Deprecated; will revert to Tamagui UI if chosen.
React UI using Tamagui (a React Native UI library).
Uses the Elm SPA client. Loaded from /elm.
An X (Twitter) account connected as a [`SyncDestination`](#rellm-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`](#rellm-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.
Used in:
The account's @username, populated by the server when the connection is made.
The account's numeric X user ID, populated by the server when the connection is made.
Only used (and required) on [`CreateSyncDestination`](#grpc-api-CreateSyncDestination): the OAuth authorization code from the X login popup. Never populated in responses.
Only used (and required, alongside `authorization_code`) on [`CreateSyncDestination`](#grpc-api-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.
X (Twitter) authentication configuration for the server. See `FederationInfo.x_twitter_auth_config`.
Used in:
The X Developer App's Client ID for the server.
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.