PDS integration overview#
End-to-end flow#
-
Stripe checkout
- User selects a plan on the pricing page.
- Plan context is passed via URL params (
auto_checkout,pds_plan,pds_disksize_gb, etc.) through/signupor/loginto/dashboard. DashboardClientcallscreateSubscriptionCheckout(priceId, options)with:username(optional, normalized)hostname(optional, cleaned; falls back to<username>.eny.k8s.frx.pub)disksizeGb(derived from plan or override)
createSubscriptionCheckoutencodes this into Stripe Checkout metadata:user_id,user_emailpds_usernamepds_hostname_basepds_disksize_gb
-
Stripe webhook → provisioning
- Stripe sends
checkout.session.completedto/api/webhooks(Vercel URL, optionally withx-vercel-protection-bypassquery param). - The webhook:
- Verifies the event using
STRIPE_WEBHOOK_SECRET. - Stores
user_id ↔ stripe_customer_idinsubscriptions(minimal mapping). - Derives provisioning parameters:
pds_username(normalized from metadata oruser_email)pds_hostname_base(metadata or<username>.eny.k8s.frx.pub)disksizeGb(metadata or"10").
- Calls
provisionPdsForUserwith:userId,userEmailpdsUsername,pdsHostnameBase,disksizeGb.
- Verifies the event using
- Stripe sends
-
provisionPdsForUser-
Uses Supabase admin client (
NEXT_PUBLIC_SUPABASE_URL,SUPABASE_SERVICE_ROLE_KEY) to read/writepds_services. -
Idempotency:
- If
pds_servicesalready has a row with a non-nullpds_service_id, it skips redeploy. - If
statusis not in{ "deploy_failed", "deploy_succeeded_no_id" }, it also skips redeploy. - Otherwise it proceeds to deploy again.
- If
-
Calls
POST https://k8s-pds.frx.pub/api/v1/deploywith BearerPDS_API_TOKENand JSON body:{ "username": "<pdsUsername>", "password": "<generated base64url>", "email": "<userEmail>", "hostname": "<pdsHostnameBase>", "disksize": <number in GiB> } -
Backend currently expects
hostnameto be a bare FQDN (no scheme, no path). -
Expects a 2xx response with JSON containing an id somewhere. Current backend returns:
{ "...": "...", "serviceId": 800 } -
The code extracts
maybeServiceIdfrom (in order):service_idserviceIdidservice.iddata.iddata.serviceId
-
It upserts into
pds_services:user_idpds_service_id(number ornull)hostnamestatus:"provisioning"whenpds_service_idis set."deploy_succeeded_no_id"when deploy succeeded but no id was found."deploy_failed"on deploy error.
-
Database schema (pds_services)#
CREATE TABLE IF NOT EXISTS pds_services (
user_id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
pds_service_id BIGINT,
hostname TEXT,
status TEXT NOT NULL DEFAULT 'provisioning',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
ALTER TABLE pds_services ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "Users can view own pds services" ON pds_services;
CREATE POLICY "Users can view own pds services"
ON pds_services
FOR SELECT
USING (auth.uid() = user_id);
CREATE OR REPLACE FUNCTION update_pds_services_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
DROP TRIGGER IF EXISTS update_pds_services_updated_at ON pds_services;
CREATE TRIGGER update_pds_services_updated_at
BEFORE UPDATE ON pds_services
FOR EACH ROW
EXECUTE FUNCTION update_pds_services_updated_at_column();
- One PDS per
user_id(PK onuser_id). statusvalues used today:"provisioning","deploy_failed","deploy_succeeded_no_id".
PDS service fetch (app/api/pds/service/route.ts)#
-
Authenticates the current user with the normal Supabase client.
-
Reads
pds_servicesrow foruser_id:- If no row or
pds_service_idisnull, returns404with a simple message.
- If no row or
-
Builds
GETURL to PDS API:GET https://k8s-pds.frx.pub/api/v1/service/{pds_service_id} -
Sends:
Accept: application/jsonX-Requested-With: XMLHttpRequestAuthorization: Bearer PDS_API_TOKEN
-
If
content-typeis JSON:- Parses body.
- Handles double-encoded JSON strings by
JSON.parsewhen possible. - On non-2xx, returns
502with{ error, status, body }for debugging. - On 2xx, returns the JSON as-is to the dashboard.
-
If non-JSON, returns
502with{ error, status, contentType, bodyPreview }.
Dashboard UI#
Usage summarysection usesServiceDetailsClient mode="stats"and is fed from/api/pds/service(or mock whenPDS_USE_MOCK=true).- Details section uses
ServiceDetailsClient mode="details":- Shows
id,service,namespace,state,kubeconfig_id. - Shows
encrypted_configfields (hostname, email settings, storage size) withadminPasswordmasked. - Shows
install_cmdand timestamps.
- Shows
Env vars (server vs client)#
-
Server-only (secret):
SUPABASE_SERVICE_ROLE_KEYSTRIPE_SECRET_KEYSTRIPE_WEBHOOK_SECRETPDS_API_TOKEN
-
Client-safe (
NEXT_PUBLIC_*):NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEYNEXT_PUBLIC_STRIPE_PUBLISHABLE_KEYNEXT_PUBLIC_STRIPE_PRICE_ID
These are configured in Vercel for all environments (or at least Preview/Production). Webhook and PDS routes only use the server-only keys.