fastco.work Pricing FAQ Live demand VM pricing Business plan Docs Setup guide πŸ–₯️ Open app

fastco.work β€” setup & wiring guide

Exactly how Google sign-in, Supabase, Cloudflare, the orchestrator and the desktop SSO are configured and connected. Reproducible from scratch. Secrets redacted throughout.

Contents
  1. The wiring map
  2. Google OAuth setup
  3. Supabase Auth setup
  4. Cloudflare & the fleet edge
  5. Portal: session & user→VM mapping
  6. Orchestrator: one VM per user
  7. Desktop SSO (no second login)
  8. Secrets inventory
  9. Verification commands
  10. Gotchas that cost time
  11. RustDesk relay (self-hosted)
  12. WhatsApp Cloud API setup

1 Β· The wiring map

Five systems, one chain. Each arrow is a real contract β€” if it breaks, sign-in fails at that hop.

  browser
     β”‚ 1. GET https://fastco.work/app                      (Cloudflare β†’ Traefik β†’ portal)
     β–Ό
  portal ── not signed in ──▢ login page (Google button only)
     β”‚
     β”‚ 2. click β†’ https://<project>.supabase.co/auth/v1/authorize?provider=google
     β–Ό
  Supabase ── 3. redirect ──▢ accounts.google.com  (client_id = OUR Google OAuth client)
     β”‚
     β”‚ 4. Google redirects to  https://<project>.supabase.co/auth/v1/callback
     β”‚    ← this URL must be in Google Console "Authorized redirect URIs"
     β–Ό
  Supabase mints a session, 5. redirects to https://fastco.work/app#access_token=...
     β”‚    ← fastco.work must be in Supabase "Redirect URLs"
     β–Ό
  portal  6. POST /app/api/session {access_token}
     β”‚    verifies it via GET <project>/auth/v1/user  β†’ email
     β”‚    sets our OWN signed cookie (fcw_user)
     β–Ό
  7. user presses "Start cowork"  β†’ orchestrator  β†’ clone/resume THEIR LXC
     β”‚
     β–Ό
  8. orchestrator mints a Guacamole SSO assertion β†’ desktop opens, no 2nd login
HopOwnerConfig that makes it work
2β†’3SupabaseGoogle provider enabled + client id/secret
3β†’4GoogleAuthorized redirect URI = the Supabase callback
4β†’5SupabaseSite URL + Redirect URLs allowlist
5β†’6Portaltoken verified server-side, own cookie issued
7β†’8Orchestratorper-user LXC + guacamole-auth-json

2 Β· Google OAuth setup

Project GENIS (genisdotai). A new client was created for fastco.work; the pre-existing "Web client 1" / "Desktop client 1" were left untouched.

APIs & Services β†’ Credentials. The fastco.work client sits alongside the untouched originals.
APIs & Services β†’ Credentials. The fastco.work client sits alongside the untouched originals.

Steps

  1. APIs & Services β†’ Credentials β†’ Create credentials β†’ OAuth client ID
  2. Application type: Web application  Β·  Name: fastco.work
  3. Authorized JavaScript origins β†’ https://fastco.work
    (your app's origin β€” this is the field that is easy to forget)
  4. Authorized redirect URIs β†’ https://dmugfnvjwgzjxonapbuj.supabase.co/auth/v1/callback
    Google hands the auth code to Supabase, not to fastco.work β€” Supabase performs the token exchange.
  5. Create β†’ copy the Client ID and Client secret (secret is shown once).
Create OAuth client ID β€” Web application, with the JavaScript origins and redirect URI fields.
Create OAuth client ID β€” Web application, with the JavaScript origins and redirect URI fields.
The finished client: JS origin https://fastco.work and the Supabase callback as the redirect URI.
The finished client: JS origin https://fastco.work and the Supabase callback as the redirect URI.
Client created. Note the warning: access is restricted to test users until the consent screen is published. (Secret redacted.)
Client created. Note the warning: access is restricted to test users until the consent screen is published. (Secret redacted.)

Publish the consent screen (required for real customers)

While in Testing, only listed test users can sign in β€” everyone else is blocked. Publishing makes it available to any Google account.

Google Auth Platform β†’ Audience β†’ Publish app β†’ Confirm.

Before: Publishing status = Testing, capped to a handful of test users.
Before: Publishing status = Testing, capped to a handful of test users.
After: Publishing status = In production. Any Google account can now sign in.
After: Publishing status = In production. Any Google account can now sign in.

3 Β· Supabase Auth setup

Project dmugfnvjwgzjxonapbuj, organisation fastco.work (Free plan).

a) Enable the Google provider

Authentication β†’ Sign In / Providers β†’ Google

The Google provider panel before configuration β€” note it displays the Callback URL you must register in Google Console.
The Google provider panel before configuration β€” note it displays the Callback URL you must register in Google Console.
  1. Toggle Enable Sign in with Google β†’ ON
  2. Client IDs β†’ paste the Google Client ID
  3. Client Secret (for OAuth) β†’ paste the Google Client secret
  4. Save
Provider configured with the client ID and secret.
Provider configured with the client ID and secret.

The bug that cost several attempts: the dashboard will happily save the client ID/secret while silently leaving the Enable toggle OFF. The API log showed 500s that were a red herring. Flip the toggle first, then Save β€” and verify functionally (section 9), never by looking at the UI.

b) URL configuration

Authentication β†’ URL Configuration. Defaults point at http://localhost:3000 and an empty allowlist, so Supabase will refuse to redirect back to your app until this is set.

FieldValue
Site URLhttps://fastco.work/app
Redirect URLshttps://fastco.work/**
Site URL set to the app.
Site URL set to the app.
Redirect URL allowlist β€” the wildcard covers the app and any sub-path.
Redirect URL allowlist β€” the wildcard covers the app and any sub-path.

c) Why Supabase and not Cloudflare Access

Access is seat-metered: free to 50 users, then $7/user/month, and once seats are exhausted new users are hard-blocked. That breaks self-serve signup and inverts the unit economics. Supabase is unlimited at ~$0 marginal cost — and we keep our own user table, which billing and the user→VM mapping need. Access is still used for admin surfaces, where the user list is small and known.

4 Β· Cloudflare & the fleet edge

One wildcard does all per-session routing, so publishing a new desktop URL needs no Cloudflare API call.

Record / ruleValue
DNSfastco.work, www, *.fastco.work β†’ proxied CNAME β†’ tunnel
Tunnel ingressall three hostnames β†’ http://192.168.1.99:8090 (Traefik)
Traefikfile provider watching /opt/fleet/dynamic; one YAML per session
GateForwardAuth β†’ fleet-auth, HMAC-signed ?t= links
# a per-session route is just a file drop:
/opt/fleet/dynamic/u-<user>.yml
  Host(`u-<user>.fastco.work`) β†’ http://192.168.1.<ctid>:8080  [middleware: gig-auth]

Cookie gotcha: the gate cookie must be SameSite=None; Secure. With Lax the browser drops it inside the embedded <iframe> and the desktop pane renders a 403.

5 · Portal: session & user→VM mapping

The Supabase secret key never reaches the browser; the page only ever carries the publishable key (and after the Google-only rework, not even that).

POST /app/api/session  {access_token}
   β†’ GET  <project>/auth/v1/user   (verify with Supabase)
   β†’ email β†’ slug = <localpart>-<sha256(email)[:6]>
   β†’ sign our own cookie  fcw_user = b64(payload|exp|HMAC)

POST /app/api/start   (auth required)
   β†’ orchestrator  POST /users/<slug>/vm?size=&engine=&anydesk=

The slug is a stable hash of the email, so a given customer always resolves to the same VM and never to anyone else's.

6 Β· Orchestrator: one VM per user

golden CT121  ──(pct template)──▢  linked clones CT130-199
   Debian + nesting + /dev/kvm + dockur Windows + Guacamole

POST /users/<u>/vm    clone-or-resume, set unique IP, size, publish route, mint SSO
POST /users/<u>/stop   pct suspend  β†’ returns billed_seconds (disk persists)
DELETE /users/<u>/vm   destroy
POST /template         seal the golden image
Sizecores / memory
small2 / 4096
medium4 / 8192
large6 / 16384

7 Β· Desktop SSO β€” removing the second login

Guacamole normally shows its own login. guacamole-auth-json (already bundled in the image) accepts a signed + encrypted assertion that carries the user and the RDP connection inline β€” so there is no Guacamole account per customer and no shared admin password in the flow.

  1. Set JSON_SECRET_KEY (16-byte hex) on the guacamole service and restart.
    Log should read: Extension "Encrypted JSON Authentication" (json) loaded.
  2. Build {username, expires, connections:{...rdp...}}
  3. HMAC-SHA256(key, json) prepended to the JSON, then AES-128-CBC (zero IV), base64.
  4. POST /guacamole/api/tokens with data=<blob> β†’ authToken
  5. Open /guacamole/#/?token=<authToken> β€” lands straight in the desktop.

The gate passes the deep link through as ?t=<hmac>&next=/guacamole/#/?token=..., so the session is authorised and the desktop opens in one hop.

8 Β· Secrets inventory

SecretWhere it livesReaches browser?
Google client secret/opt/fleet/.supabase.env (0600) + Supabaseno
Supabase secret key/opt/fleet/.supabase.env (0600)no
Supabase publishable keyportal envyes (by design)
Fleet HMAC key/opt/fleet/.secret (0600)no
Control-plane API key/opt/fleet/.secret.env (0600)no
Guacamole JSON SSO keyper-VM compose envno

Outstanding: the SSO key and the Windows account password are still shared placeholders inherited from the golden template. Randomise them per clone before real customers, and rotate every credential that has been pasted into a chat or terminal.

9 Β· Verification commands

Verify functionally β€” a green dashboard toggle is not evidence.

# Is Google really enabled?
curl -s -H "apikey: <PUBLISHABLE>" https://<project>.supabase.co/auth/v1/settings
#   expect: "external": { "google": true, ... }

# Does the OAuth hop actually work?
curl -s -o /dev/null -w "%{redirect_url}" \
  "https://<project>.supabase.co/auth/v1/authorize?provider=google&redirect_to=https%3A%2F%2Ffastco.work%2Fapp"
#   expect: 302 β†’ accounts.google.com/... with OUR client_id

# Is the app gated?
curl -o /dev/null -w "%{http_code}" -X POST https://fastco.work/app/api/start     # 401
curl -o /dev/null -w "%{http_code}" https://u-<user>.fastco.work/guacamole/       # 403

# Fleet health
docker ps --filter name=fleet ; systemctl status fastco-orch

10 Β· Gotchas that cost time

SymptomCauseFix
Unsupported provider: provider is not enabledSupabase Google toggle silently off despite saved credentialsflip toggle, then Save; verify via /auth/v1/settings
Desktop iframe shows 403 / brokengate cookie was SameSite=LaxSameSite=None; Secure
Every user lands on the same IPclones inherit the template's static IPpct set <ctid> --net0 ...ip=192.168.1.<ctid>/24
Cannot do full clones on a running containergolden template still runningstop it, then pct template
Google returns access_blockedconsent screen still in TestingAudience β†’ Publish app
OSError: [Errno 16] Resource busyatomic replace over a bind-mounted filebind-mount the directory instead
Cloudflare token reads but cannot write DNS"Entire Account" scope selected but zero permissions tickedtick DNS Write + Tunnel Write; /user/tokens/verify is unreliable for cfat_ tokens

11 Β· RustDesk relay (self-hosted)

Two containers from /opt/fleet/rustdesk/compose.yml: rustdesk-hbbs (rendezvous) and rustdesk-hbbr (relay). The public key clients must trust lives at /opt/fleet/rustdesk/data/id_ed25519.pub.

The golden bakes the client and writes RustDesk2.toml with custom-rendezvous-server, relay-server and key already set, then deletes RustDesk.toml. That deletion matters: the ID lives in that file, so leaving it in the golden would give every linked clone the same RustDesk ID. Clearing it forces each clone to mint its own on first service start.

To push RustDesk into an already-running VM without a re-bake:

python3 /usr/local/bin/rustdesk-provision.py <CTID>
# writes provision.cmd; the in-guest agent runs it within ~60s and reports back
cat /opt/cowork/shared/rustdesk-id.txt      # inside the CT: the minted ID

Results are written to the shared folder, not C:\fastco, because reading the guest's NTFS disk while the VM runs returns stale data β€” a trap that previously caused a working build to be declared failed.

12 Β· WhatsApp Cloud API setup

Create /opt/fleet/.wa.env (mode 0600):

WA_TOKEN=...        # permanent token from the Meta app
WA_PHONE_ID=...     # phone number id, not the phone number
WA_APP_SECRET=...   # used to verify X-Hub-Signature-256

Point the Meta webhook at https://fastco.work/wa/webhook with verify token fastco-verify. The handshake returns the challenge on success and 403 on a wrong token, so a misconfiguration fails loudly rather than silently accepting traffic.

Get in touch

Questions the FAQ didn't cover, or interested in the customizable Max plan?
Message us on WhatsApp or call us at +66 86 954 1666

Β© fastco.work