Self-hosting levels
Three ways to run internkim, from the host on your own computer to a host you can reach from any network.
A company can run as much of internkim as it wants and leave the rest to
intern.kim. The levels below name the common stopping points.
| Level | The host | Supabase | Cloudflare | Reached from other networks |
|---|---|---|---|---|
| 1 | your computer | intern.kim | intern.kim | no |
| 3 | your computer | yours | yours | no |
| 4 | your computer or a cloud server | yours | yours | yes, over SSH |
Level 1: the host on your own computer
Install the host on a computer the company leaves on,
such as a Kim mini, a Mac or a Linux machine. The company's record, the web app and the
sign-in address stay with intern.kim. The host connects outward, and once it
belongs to a company nothing listens off loopback, so the computer needs no
public address.
Level 3: your own Supabase and your own Cloudflare
The record moves to a Supabase project you run, and the web app is deployed to
your Cloudflare account and served from your own domain, which
CLOUDFLARE_DOMAIN names. Deploying the web app yourself is what lets it read
your project's address and keys from its settings. What lives where
says which data that project holds. Self-hosted Supabase ships the same
Postgres and the same row level security, so the schema in supabase/migrations
applies unchanged.
Everyone signs in at your domain itself, so sign-in and the web app no longer
depend on intern.kim. Your web app serves install.sh as well, and the host
package that script installs still comes from our GitHub Releases.
Deploying the web app
Which Supabase project the app talks to is decided when it runs: hooks.server.ts
injects SUPABASE_URL and SUPABASE_PUBLISHABLE_KEY into the page, read through
$env/dynamic/private. One build therefore serves any company. The Pages
project carries every variable tools/environment.json marks
isRequiredOnPages, and web/scripts/set-pages-secrets.ts sets them all as
secrets.
Among them, SUPABASE_JWT_SIGNING_KEY is the private key the web app signs
personal access tokens, agent sessions and host sessions with.
web/scripts/issue-record-signing-key.ts mints one and prints it; import it in
the Supabase dashboard under Project Settings → JWT Keys as a standby key, so
the project verifies what the app signs.
A production deploy first checks that every migration in supabase/migrations
is applied to your project, through the Supabase CLI. Sign in with your own
supabase login and run supabase link --project-ref <ref> before deploying;
no Supabase access token is kept with the project's keys.
A release therefore goes in one order: supabase db push, then the web app,
then the edge functions and the gateway. Each migration is written to hold while
the previous web app is still running, so one that asks more of a session
accepts the sessions issued before it until they expire.
cd web
bun install
bun run build
bun run scripts/deploy-pages.ts --project <your-pages-project> --output .svelte-kit/cloudflare --productionThe script reads CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID, and deploys a
preview unless --production is passed. Custom domains always serve the production
deployment, so a preview answers only on *.pages.dev. Each hostname is attached
explicitly with scripts/pages-domains.ts, because Pages custom domains do not take
wildcards.
- Store every Pages variable as a secret, including the public ones.
wrangler pages deployrewrites plain-text variables from its own configuration, so a plain-text value can vanish on the deploy that was meant to start using it. api.<zone>attaches to the same project, because the public API is the same routes.GATEWAY_URLandGATEWAY_ADMIN_TOKENlet an API call reach the company's computer through the connection gateway. Without them an invocation answers503, while the catalog and the tokens still work.- A company whose people use the Buzz desktop or phone app gives the gateway its
messenger address,
<slug>.<zone>, as a Workers route. That hostname belongs to the messenger and is not attached to Pages, because the relay answers paths under/api/of its own. A page opened there in a browser is still sent on to the zone with a308. Deploy the gateway withweb/scripts/deploy-worker.ts workers/connection-gateway --route-subdomain <gateway> --route-subdomain <slug>, naming every route it keeps, since a deploy replaces the set. supabase config pushsends the wholeconfig.toml, so any setting the file omits returns to the CLI default. Read the diff it prints before trusting the exit code.
Level 4: reachable over SSH from any network
The host still opens no way in, and the product provides none. A level 4 company chooses its own route to the computer:
- a Cloudflare Tunnel from your own account, with an Access policy in front of
it, or a quick tunnel from
trycloudflare.comfor a single session - the host on a cloud server that already has a public address
The tunnel or the server is the operator's. ~/.ssh/config names the route, as
for any host.
Through your own Cloudflare Tunnel
We reach our own company's host this way. On the host, a tunnel from your
Cloudflare account carries one public hostname to ssh://localhost:22. In Zero
Trust, a self-hosted Access application on that hostname admits your operators
when they sign in, and a Service Auth policy admits a service token, which
scripts use because it needs no browser. From any network:
Host ssh.example.com
User <account on the host>
ProxyCommand cloudflared access ssh --hostname %h --service-token-id <client id> --service-token-secret <client secret>Leave out the two token flags and cloudflared opens a browser sign-in
instead. In this repository, tools/cloudflared-access-ssh is that
ProxyCommand with the token read from the vault, and ./internkim @host ssh
connects through it.