Granting Logins & Access โ
What a POC does for a new person, in order. One pass each, USERNAME / EMAIL being theirs.
Grant when โ
| Grant | Unblocks | When |
|---|---|---|
| Admin account | The portal, the docs site that carries the onboarding page, and their app invite | First |
| GitHub invite | Cloning at all | Before they start |
| Secret Manager | npm run env:bootstrap, backend work | When they touch server-side code |
| GCP console, read-only | Firestore data, Auth users, logs, BigQuery | When they need to see production-shaped data |
| GCP per-service write | The specific job in front of them | On request, one role at a time |
| Vendor dashboards | Cloudflare, Railway, Prelude, Resend, Anthropic, Discord | Only when their work reaches that vendor |
1. Admin account, which also hands you their app invite โ
Do this first. It is how they read the onboarding page.
Create their account from the admin portal's Users tab, Create Admin. The success screen shows two links, and you want both:
- Admin Portal Setup (
?mode=adminReset&token=..., 24 hours, single use): where they set their admin portal password. A setup email goes out too; this link is the backup for when it does not arrive. - Main App Invite Link (
#/admin-signup?token=..., 7 days, single use): their invite to the Lantern app itself. Signup is invite-gated, so without it the app dead-ends for them.
Copy the app invite while it is on screen. It is issued once, at creation, and the portal has no button to reissue it yet (#850).
Then send them the docs site: View docs site in the portal, and point them at Engineering > Guides > Onboarding. That is their whole set of instructions from here.
Say one thing when you send it: the admin portal password is separate from the Lantern app passphrase and has to stay that way. The app passphrase is also their encryption key, so a routine admin password reset would destroy their app data (why).
2. GitHub invite and Secret Manager โ
One command does both:
npm run onboard:grant -- --github USERNAME --email EMAILIt shows you each command, asks before applying, and prints the remaining click-only steps. Add --dry-run to preview, or pass one flag to do one grant. Idempotent, so re-running after a partial failure is safe.
- GitHub invite is the only grant that blocks them from cloning and running the app.
- Secret Manager only matters when they reach backend work; it unblocks
npm run env:bootstrap.
By hand, if you would rather:
gh api -X PUT repos/cattreedev/lantern_app/collaborators/USERNAME -f permission=push
gcloud projects add-iam-policy-binding lantern-app-dev --member="user:EMAIL" --role="roles/secretmanager.secretAccessor"3. Google Cloud console โ
Needed to see Firestore data, Auth users, Cloud Run logs, and BigQuery. Read-only to start, then a per-service grant when a specific job needs write:
gcloud projects add-iam-policy-binding lantern-app-dev --member="user:EMAIL" --role="roles/viewer"
gcloud projects add-iam-policy-binding lantern-app-dev --member="user:EMAIL" --role="roles/firebase.viewer"roles/viewer deliberately does not include reading secret payloads, which is why step 2's secretAccessor is its own grant.
The per-service grants that actually come up, one at a time as the work arrives:
| Role | For |
|---|---|
roles/datastore.user | Writing Firestore documents (backfills, migrations) |
roles/bigquery.dataEditor | Dataform and the billing pipeline |
roles/secretmanager.secretAccessor | Already covered by step 2 |
Nobody needs a deploy role: deploys go through GitHub Actions, never a person.
4. Vendor dashboards โ
No CLI for any of these, and none are needed on day one. Invite from the vendor's own dashboard when their work reaches it, at the narrowest role that dashboard offers.
| Vendor | Covers | Grant when |
|---|---|---|
| Cloudflare | Pages deploys, DNS, Zero Trust Access policies | They work on hosting, domains, or the docs host |
| Railway | Hosts the Discord triage bot | They work on the bot |
| Prelude | OTP delivery. Test numbers only, never a real phone | They start OTP work |
| Resend | Transactional email | They work on email flows |
| Anthropic | API keys and billing for the assistant and triage | They work on an AI surface |
| Discord | The triage bot and its webhooks | They work on the bot or alerting |
Production access โ
Anything production-connected needs sign-off from a security reviewer, not just your grant. Confirm they have done the security checklist in Part 4 of ONBOARDING.md first, and grant the narrowest role that unblocks the work in front of them. Same for vendors and contractors, who usually need one of these grants and none of the others.
Serving docs to someone WITHOUT an admin account
docs.dev.ourlantern.app carries the same content behind Cloudflare Access, so it needs an explicit email allow. Only worth it if you are skipping step 1.
The Zero Trust dashboard is the safe path: Access > Applications > the app covering docs.dev.ourlantern.app > Policies, add the email to the allow policy.
From the terminal, the Access API replaces a policy's whole include list rather than appending to it, so a one-liner has to read the current list, add the email, and write it back. With CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID in your .env.local:
set -a && . ./.env.local && set +a
APP=$(curl -s "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/access/apps" -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" | python3 -c "import json,sys;print([a['id'] for a in json.load(sys.stdin)['result'] if a.get('domain','').startswith('docs.dev')][0])")
curl -s "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/access/apps/$APP/policies" -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" | python3 -c "import json,sys;[print(p['name'], p['id']) for p in json.load(sys.stdin)['result']]"That prints the policy names and ids. Then fetch the policy you chose, append the email to include, and PUT it back. Check the result in the dashboard afterward: a malformed include list replaces who can reach the host. The repo's token is confirmed to READ Access apps and policies; its write scope has not been exercised.