Friend Onboarding

A self-serve external-user platform behind my own front door.

self-hosted access OAuth identity role-gated provisioning invite lifecycle forward-auth outbound-only home path Flask request workflows

The short version

Friends join a self-hosted media library and, where granted, the local AI assistant through a single invite link. The system verifies identity, auto-provisions access to Plex and Discord, and gates assistant sessions so each person reaches only their own workspace. Nothing in this platform listens for connections at my house. A cloud droplet is the only public-facing surface, and the home side reaches it by opening connections outbound over Tailscale.

It has run in my lab since 2026-07-05, serving my friends and family. Fifteen people use it besides me: five with assistant access (the three nursing and law pilots and two friends on the general assistant), and ten on media only.

What a friend experiences

The onboarding flow runs in six steps:

  1. Invite-code gate. I mint a single-use, 30-day-expiring code from the admin panel and send the link. The code carries that friend's capabilities: media access by default, assistant access when I choose. If a friend returns later with an unfinished flow, they resume automatically on re-authentication instead of needing a fresh code.
  2. Sign in. Google OAuth is the primary path. Friends who use Apple or typed-email accounts hit a fallback that accepts their email directly. There is no Sign in with Apple button: it would require a paid developer account and Apple's per-app relay email differs from the friend's Plex address, breaking the matching logic.
  3. Plex auto-invite. The captured email receives a Plex share through the API. Only the Movies and TV libraries are shared.
  4. Discord (recommended, skippable). A friend who joins gets an auto-assigned member role from the server bot. Their Discord ID links to their onboarding record for attribution and "your request is ready" notifications. Someone who skips keeps full function through the web request page; they just lose chat pings.
  5. Request tool import. The request tool is the self-hosted page where friends ask for a film or a show. Signing into it with Plex auto-imports the user with a default permission set and maps their display name to the invite label.
  6. Tutorial redirect. On completion, the friend reaches the tutorial page. It covers how to request content, how to watch on each device, and where to get help.

What I get as the admin

Before this existed, each new person meant the same hand work in several places: the media share, the request tool, the chat server role, and the assistant's user setup. Now it is one link and one checkbox.

A single per-friend status dashboard tracks four stages: invited, accepted Plex, signed into the request tool, Discord linked. It flags mismatches when a multi-account user picks the wrong identity. Each stage is recorded against the email key, so a closed browser or a failed step leaves the record intact and the friend resumes where they stopped after signing in again. The dashboard shows which stage a friend is stuck on, and an admin view-as mode lets me see the flow the way they see it.

Minting a code is one click. The panel returns a ready-to-send link that auto-fills the join form with the correct code and capability set. Adding assistant access is explicit: I check a capability flag and the provisioning adapter handles the rest. Plex invites, request-tool imports, and Discord role assignments all happen automatically.

The admin panel requires a Google-verified admin email, password login is disabled, and an identity that arrived through the email fallback is never treated as an administrator.

How it reaches the internet

Friends reach the platform through three public DNS records. Traffic follows this path:

HopWhat happens
Public DNS A recordsPoint to a static public IP on a cloud droplet. No dynamic DNS needed.
Droplet reverse proxyServes HTTPS via Let's Encrypt, issued and renewed automatically. This is the only public-facing web surface.
Tailscale overlayA private overlay to the home network. It fills the same role as a vendor tunnel service, with the traffic path under my control.
Home reverse proxySees only outbound-established traffic. Home infrastructure ports stay closed.
ApplicationsThe onboarding app, the request tool, and the assistant orchestrator behind it.

Media streaming is the exception. It still uses the media server vendor's relay service, outside this path, and I do not route it through the same front door, so the platform's outbound-only claim is about the onboarding, request, and assistant traffic, not about streaming. I chose this architecture so the public edge and the certificates stay under my control and I did not have to move nameservers. Tailscale still relies on its own coordination service, but the tunnel itself is direct and no third party terminates or inspects the traffic.

The trade-off is that there is no managed Web Application Firewall in front of it. What stands in for one is a small attack surface. SSH on the droplet is reachable only over the overlay. The firewall exposes only the HTTPS port to the public and locks the DNS port to known addresses. A default-deny catch-all makes the proxy answer only the names it serves, and the admin interfaces never face the public.

Identity and gating

Email is the primary key, captured at join time. Three authentication providers are recognized: Google, Plex, and typed email. Only Google and Plex count as verified; a typed email is never treated as proof of identity, gets no personalization, and cannot reach admin features. A typed email is enough to receive a media share, because the code is single-use and went to one person and a share can be revoked. It is not enough to reach the assistant or the admin panel, both of which require a verified identity.

The assistant's front door uses a forward-auth gate at the edge. An unauthenticated request is redirected into the onboarding start flow. Only a verified admin or a provisioned friend passes through. The gate sets an authenticated-identity header that tells the orchestrator which user to pin sessions to. The proxy strips any client-supplied copies of this header before the forward-auth check runs, so a user cannot forge another identity.

Per-user assistant provisioning is handled by a data-driven adapter. When a friend receives assistant access, the system cross-references their persona (general, nursing, or law), creates their private namespace, and notifies me via Discord. Existing users were backfilled by a script run first as a dry run, then with a commit flag.

Components and current numbers

The onboarding application is one small Flask container deployed in the same Compose stack as the media apps. It follows the lightweight-admin pattern used elsewhere in the lab. The modules:

ModuleResponsibility
Web routes, one set per public nameGoogle OAuth, typed-email fallback, resumable flow completion.
Forward-auth endpointIdentity verification for the assistant front door.
Admin mint panelCode generation with capability checkboxes; ready-to-send link output.
InvitesGranular caps, single-use enforcement, 30-day expiry tracking.
Email-keyed storeResumable records keyed to identity. Onboarding resumes automatically after re-authentication.
Plex integrationPIN sign-in, owner share of Movies and TV, runtime resolution of library section IDs.
Request-tool importDisplay name mapped to the invite label; default permission set applied.
Assistant-persona moduleCross-reference against general / nursing / law personas, provisioning adapter, Discord notification pings.

Current usage across the platform:

MetricCount
Provisioned assistant profiles (all personas)5
Friends on media-only access10
Public DNS records for the front door3

The persona system (general / nursing / law) drives which grounding corpus each assistant user reaches. See that page for the two-layer design: persona-shared reference corpora plus per-user writable namespaces.

Current Lab overview · Media stack architecture · Home AI Orchestrator · Persona pilots