RumbleTab
Leaderboard-first scorekeeping for in-person card nights is available at rumbletab.com.
Why it exists
RumbleTab began as the scorepad for my family's house rummy variant and developed into a product available at rumbletab.com. A recurring card night creates a history worth retaining, but the paper scorepad used to settle the night is discarded along with the group's ongoing record. RumbleTab retains this record, with the group serving as the durable unit. Each night played contributes to the group's leaderboards, while an activity feed preserves the results so that Tuesday's game continues to be argued about on Wednesday morning.
The product also has explicit boundaries because it does not support gambling, stakes, or real-money play. The competition concerns only standings and bragging rights.
What it is
One Go API serves as the system of record and the rules authority for two first-class clients. The SwiftUI iOS app operates offline first and maintains a local mirror of the group in GRDB, which allows an entire night to be opened, scored hand by hand, and closed without connectivity before syncing upon reconnection. The Next.js web client provides the same product surface, along with an operator console for moderation, announcements, and the signup entry gate. For each night, a member opens a session, seats the players, records every hand, and closes the night, with the lowest total determining the winner. The surrounding features include persistent groups, live boards, four leaderboards, an activity feed with comments and reactions, an achievements engine that runs when a session closes, friendships across group boundaries, and Apple push notifications.
An early schema decision was particularly important. Because real card nights include people who will never install an app, a player is not required to have an account. A guest is a persistent participant within a group who can be seated across many nights, accumulates statistics like any other player, and can be claimed later if the person eventually signs up.
How it works
The system centers on an append-only events table. Every mutation inserts its event row in the same transaction as the associated change. A counter on the group row allocates a per-group sequence number, which provides gapless ordering within each group and avoids the effects of clock skew between writers. The table supplies the replay cursor for the live stream, pagination for the activity feed, and the checkpoint for the iOS sync engine.
Live delivery uses Server-Sent Events. Each persisted event is sent with its database row id as the SSE id. When a client reconnects, it provides the last id received, and the server replays every newer event before adding the client to the live flow, with deduplication across the transition. Presence updates intentionally omit ids and therefore never advance the replay cursor. A short heartbeat maintains idle connections through proxies. The streaming routes use a dedicated edge gateway with route timeouts disabled, separate from the gateway for ordinary request traffic.
To support fan-out beyond a single process, the events table also serves as a transactional outbox. A poller within the API binary tails the table into Kafka and uses a Postgres advisory lock for leader election. Heavier change-data-capture machinery was evaluated but deferred until relay lag becomes a measured problem.
Where it runs
It uses a self-managed Talos Linux cluster comprising Graviton arm64 instances across three availability zones, with Cilium providing both the CNI and the Gateway API implementation. Flux reconciles the cluster from a separate infrastructure repository, whose layers are ordered by dependency so that a cold rebuild proceeds in sequence. Postgres runs through CloudNativePG with three instances under quorum synchronous replication and continuous backups to S3, while Kafka runs through Strimzi.
The platform self-hosts nearly every component that would usually be purchased as a managed service. Keycloak provides operator identity, including kubectl single sign-on with OIDC group claims, whereas product users authenticate through Clerk. OpenBao stores secrets, which External Secrets Operator projects into namespaces. NetBird provides the private network path to the clusters and operates with its own TURN infrastructure. The full Grafana stack supplies observability, with Loki for logs, Mimir for metrics, Tempo for traces, and Alloy for collection. Deployments from GitHub Actions authenticate through OIDC, so they require no long-lived cloud keys. Whenever the application repository releases a new image, a promotion bot opens a pull request against the infrastructure repository.
The platform includes separate supporting infrastructure for messaging. Transactional email is sent through Amazon SES. The platform also operates its own mail server inside the cluster, which provides SMTP, IMAP, and JMAP services for its domains, stores its state in the shared Postgres, relays outbound mail through SES for deliverability, and receives inbound mail through a dedicated network load balancer. Apple push notifications are delivered directly, with the signing key projected from the secret store into the API in the same manner as other credentials.
Text messaging uses AWS End User Messaging with a registered 10DLC campaign, which United States carriers require before an application can send at production volume. The API receives permission to send through an OIDC-assumed role scoped by resource tag, so it can originate messages only from phone numbers tagged to the SMS service. The static IAM user that preceded the role was removed. Carrier-managed opt-out handling remains in place as a send-time backstop to the application's consent rules, and current work is extending the channel to two-way messaging so the platform can receive replies as well as send them.
What it led to
The work involved in operating email for RumbleTab also led to the next project. Providing transport, mailbox access, and deliverability for one platform required the assembly of several single-purpose components and management of their distinct operational requirements. This experience demonstrated a concrete gap in self-hosted mail. Mailwright addresses that gap as a single Rust server that provides mail transport, mailbox access, groupware, and spam filtering through one administration surface.
Why it matters
I wanted to a) build and deploy and iOS application. Something I had never done before. I found the development environment and experience of writing Swift very pleasurable indeed and enjoyed almost all aspects of it, except dealing with the ecosystem around publishing the actual app itself. I had the app fully complete and the infrastructure within about two months of effort, and it's taken more than a month and a half now to get through Apple's bureaucratic red tape. We are close, so look out for a rumble tab. Go live notice soon!