Skip to content

feat(cloud): email magic-link sign-in, and a real public landing page - #413

Merged
mohabbis merged 16 commits into
masterfrom
feat/browser-recording-extension
Aug 7, 2026
Merged

feat(cloud): email magic-link sign-in, and a real public landing page#413
mohabbis merged 16 commits into
masterfrom
feat/browser-recording-extension

Conversation

@mohabbis

@mohabbis mohabbis commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add email magic-link sign-in via Resend (RESEND_API_KEY/RESEND_EMAIL_DOMAIN), following the existing env-var-gated pattern. Wires the Prisma adapter (only when Resend is configured) and allowDangerousEmailAccountLinking on GitHub/Google to preserve the existing merge-by-email behavior.
  • Replace the root page's unconditional redirect-to-/signin with a real public landing page (what Ghost does + explicit sign-in link). Previously every visitor hit a bare auth wall with no context.

Test plan

  • pnpm typecheck (all packages) — clean
  • pnpm --filter @ghost/web exec eslint on changed files — clean
  • pnpm test (Postgres + Redis running) — 5/5 packages green
  • pnpm --filter @ghost/web build — succeeds both with and without Resend env vars set
  • Verified / renders real content locally (next start, curl)
  • Verified no secrets in the diff

🤖 Generated with Claude Code

mohabbis and others added 2 commits August 5, 2026 19:05
GitHub and Google both require an account with that provider; email
magic-link removes that barrier entirely. Wires next-auth's Resend
provider, gated the same way as GitHub/Google (env vars present = offered).

The Email provider is the first to need a database adapter (Auth.js
stores its one-time tokens there), so this also wires PrismaAdapter
in — but only when Resend is configured, so a deployment without it
is unchanged. GitHub and Google get allowDangerousEmailAccountLinking
so the adapter's account-linking check doesn't newly reject a user who
signed up with one provider and later tries another with the same
email — ensureUserOrg has always merged by email with no such check.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The root page redirected unconditionally to /signin, so the site's only
public face was a bare auth form with no context for what it was
gating — indistinguishable from an unfinished stub. Signed-out visitors
now see what Ghost does with an explicit sign-in link; signed-in
visitors still land straight on /dashboard.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ghost Ready Ready Preview Aug 7, 2026 6:19pm

…andoff

The Chrome extension merged to master in PR #409; this doc still said
"not yet merged." Also adds a "Make Ghost demo-able" section: the
deploy/auth/domain gaps are closed, but nothing on the live site shows
the trust loop running — the fixtures/order workflow works but isn't
visible or walkable from the public site.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
mohabbis and others added 11 commits August 7, 2026 10:57
Dashboard previously showed a static "what Ghost does" list duplicated
from the landing page, with no link anywhere in the app — a dead end
for a signed-in user with no workflows. It now points a workflow-less
org straight at creating the demo workflow, and an org with workflows
gets quick links to Workflows/Runs/Audit.

Workflows and Recordings list rows were missing flex-1 on the name
column, so it collapsed to near-zero width next to the shrink-0 button
cluster and wrapped text letter-by-letter (visible live on the
Workflows page with the demo workflow's long name).
DEPLOYMENT.md and deploy-website.yml both claimed ghost.muharafiq.com
serves the static public/ marketing site. It doesn't — the ghost-app
Vercel project's Root Directory was repointed at cloud/apps/web, which
now serves the live cloud SaaS app there directly. Since that override
applies regardless of what changed in a push, deploy-website.yml would
silently redeploy the wrong app (cloud/apps/web, not public/) if
triggered, despite its own logging claiming otherwise. Disabled its
push trigger and added warnings until public/ gets its own Vercel
target or is retired; updated DEPLOYMENT.md and CLAUDE.md to describe
what's actually live.
flex-1 sets flex-basis:0%, which the flex-wrap algorithm treats as
zero hypothetical size — so it never triggers a wrap, it just keeps
shrinking min-w-0 content down to a word-per-line column. Confirmed
live: the Workflows header and its demo row still squeezed at full
browser width even after the earlier min-w-0/flex-1 patch, with the
title/name partially hidden behind the button cluster.

Switched to the standard pattern instead: flex-wrap on the row, no
forced shrink on the title/name block, so the button/control cluster
drops to its own line once it doesn't fit, rather than crushing the
text next to it. Already deployed to production; this commit just
catches git history up to what's live.
Ghost's engine worked; getting it running did not. Three defects made a
correctly-built stack look dead, and all three were invisible — no error,
just a UI that renders and never does anything.

1. Nothing loaded `cloud/.env`. The README has always said `cp .env.example
   .env && pnpm dev`, but that file sits at the workspace root while the apps
   run from `apps/web` and `apps/worker`. Next reads `.env` relative to its own
   project directory; the worker read nothing and died on "REDIS_URL is not
   set". Every local setup that appeared to work was one where someone had
   exported the variables by hand, which is why it never reproduced for them.
   `packages/core/src/env.ts` now loads it for both, without overriding a real
   environment (so Vercel and containers are unaffected).

2. `demo.sh` treated an open port as a working service. `port_open 5432` is
   true for any Postgres — Homebrew's, Postgres.app's, another project's — and
   each accepts the connection, denies the `ghost` user, and turns every query
   into an unexplained 500 later. Setup reported success, so the step that
   caused the failure looked like the one thing that had gone right. Probing
   now runs a real query as `ghost` (and a real PING for Redis), falls back to
   a free port, and repairs a stale `.env` instead of leaving it wrong.

3. A run whose worker died stayed RUNNING forever. The lease correctly lets a
   new job take over an expired one, but nothing produced that job: when a
   worker crashed or was redeployed mid-run, the BullMQ job died with it and
   the row sat RUNNING with an expired lease indefinitely — a run in progress
   that no process was working on. `reclaimRuns.ts` sweeps on boot and every
   minute. Restarting is safe because run position is folded from the
   append-only journal, so a completed step is not re-executed; past five
   attempts the run becomes an INCIDENT for a human rather than looping.

Adds `pnpm check`: read-only, and it names these two ("NO WORKER RUNNING",
"something IS listening there — most likely a different Postgres") rather than
leaving them to be inferred.

Verified on this machine, not just in CI: `pnpm check` clean, the e2e driver
halts at the gate, captures session state, resumes, clicks submit exactly once
and reaches SUCCEEDED; two runs stuck RUNNING for 20 hours were reclaimed on
boot and correctly refused an unsafe resume (OUTCOME_UNKNOWN / RESTORE_UNSAFE)
rather than re-clicking. typecheck 5/5, 430 tests, build 4/4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…al env docs

Signed-out visitors previously landed straight on /signin with no explanation
of what Ghost does; now they see the actual pipeline, positioning, and target
verticals before being asked to sign up. Also routes Auth.js's sign-out
confirmation through a themed page instead of its unstyled built-in one, and
documents the REDIS_URL/GHOST_SESSION_KEY env vars pnpm test silently needs
(their absence surfaces as 429s/INCIDENT status, not a missing-var error).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
cloud/packages/core/src/recording/ingest.ts was staged in the index by
in-progress work from a parallel session (not part of the landing-page/
sign-out/docs change). It got committed as a byproduct of `git add`
committing the whole index rather than just the intended files. Untrack it
again so it goes back to being that other work's own untracked file, with
no change to its on-disk content.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Production had no S3_BUCKET/GHOST_ARTIFACT_DIR configured, so
artifactStore() fell back to writing to local disk — which doesn't
work on Vercel's read-only serverless filesystem and made every
recording upload 500. Add VercelBlobArtifactStore (private access,
reads always go through the app's own artifact route since signed
URLs aren't safe to hand out for a private store) and prefer it when
BLOB_READ_WRITE_TOKEN is set.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
globals.css and layout.tsx got swept into the prior commit by a race
with another concurrent agent process editing this same working tree
(staged those two files between my `git add` and `git commit`). Neither
belongs to the Blob-storage change that commit describes, and I never
reviewed their content. Restoring both to their pre-commit state here;
their in-progress edits are reapplied to the working tree, uncommitted,
in the next step.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
get() can return {stream: null} on a 304 (or null entirely if not
found); narrow on result?.stream so a missing/empty read fails loudly
instead of iterating a null stream. Caught by tsc, not runtime.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
master shipped a substantially more complete landing page and dashboard
(#414, #415) while this branch had its own smaller landing-page pass in
flight — same files (page.tsx, dashboard/page.tsx, globals.css). Resolved
in favor of master's version everywhere they overlapped: it's more honest
(built/not-built status, the "is this just browser automation" section),
fixes a real dark-mode CSS bug (@theme nested inside @media doesn't survive
Tailwind v4's hoisting), and renders real org data on the dashboard instead
of static nav cards. Dropped the Fraunces/IBM Plex Mono font experiment
from layout.tsx/globals.css along with it, since nothing references those
tokens anymore and master's design intentionally stays on one font.

Kept from this branch: the themed sign-out page (signout/page.tsx,
auth.ts pages.signOut) and the CLAUDE.md env-var doc fix, neither of
which master touched.
The DOM ReadableStream type in this tsconfig's lib (ES2022, no DOM)
has no Symbol.asyncIterator, so `for await` over it doesn't typecheck
even though it works at runtime on Node 24. Route through
Readable.fromWeb instead, which is typed and portable regardless of
tsconfig lib settings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The regex required `providers.push(GitHub)` — a bare reference — but this
branch legitimately calls `GitHub({ allowDangerousEmailAccountLinking: true })`
(needed since the Resend adapter went in: without it, Auth.js refuses a
sign-in whose email already belongs to a different provider's account).
Match on the provider name after `providers.push(`, not on whether it's
invoked — the actual guarantee under test (each provider gated behind its
own env var) is unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@mohabbis
mohabbis merged commit db8ef20 into master Aug 7, 2026
26 of 27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant