Skip to content

feat: remove portless in favor of fixed development ports - #151

Open
adelrodriguez wants to merge 3 commits into
mainfrom
t3code/optimize-portless-setup
Open

feat: remove portless in favor of fixed development ports#151
adelrodriguez wants to merge 3 commits into
mainfrom
t3code/optimize-portless-setup

Conversation

@adelrodriguez

@adelrodriguez adelrodriguez commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

This started as "optimize the portless setup" and ended with deleting it. Investigating the optimization angles showed the proxy wasn't paying for its complexity: its flagship package route (db.init.localhost) had never worked — drizzle-kit studio serves a headless 404 API whose real UI lives at local.drizzle.studio — and every workspace carried a wrapper script, a config block, PORTLESS_URL env plumbing, and rename tooling just to keep the hostnames alive.

Every app already declared a unique PORT default in its .env.schema — the same assignments the repo used before portless landed in #129 — so dev servers now just run their framework commands on those fixed ports:

Port Server Port Server
3000 api 3004 docs
3001 app 3005 extension
3002 mobile 3006 web
3003 desktop

Package dev servers take the 4000 block sequentially: Drizzle Studio on 4000 (UI at local.drizzle.studio), email preview on 4001, Inngest on 4002. Docker Compose is untouched — redis, postgres, and minio on 8000–8003, as before.

Along the way:

  • The Inngest dev server moved from portless port-shifting to a one-line dev script in packages/workflows that polls the api at a stable http://localhost:3000/workflows. A Docker Compose variant was built and tested first, then rejected: it requires network_mode: host to call back into host dev servers, which is firewall- and platform-fragile for a distributed template. Drizzle Gateway in compose was likewise tested and rejected in favor of Studio's code-aware schema view.
  • PORTLESS_URL fallback logic in the api base-url/CORS helpers and the app auth config collapsed to plain env reads; the schema declarations, turbo env entry, and generated types are gone.
  • bun template rename and add no longer rewrite hostnames or route names; scripts/template/portless.ts is deleted.
  • Verified end to end: api, web, Studio, and Inngest boot on their ports, and an Inngest demo event executes through the polled endpoint.

One deliberate trade-off: two projects' dev fleets can no longer run at the same time without port collisions — that was portless's genuine feature, and this chooses simplicity over it.

Written by Claude Fable 5 via Claude Code.

🤖 Generated with Claude Code

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 20, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
init Ready Ready Preview Aug 20, 2026 7:40pm
init-docs Ready Ready Preview Aug 20, 2026 7:40pm

Request Review

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No blocking issues. Two rough edges inline, one design consequence worth a decision.

Reviewed changes — full read of the 34-file diff for commit b5ff345, plus the surrounding auth, Hono, Turbo, and template-script code that the deleted PORTLESS_URL plumbing fed into.

  • Portless deleted repo-wideportless dropped from root devDependencies and bun.lock; every workspace's "dev": "portless" + "dev:app" pair collapses into one dev script and loses its portless: { name, script } block.
  • PORTLESS_URL plumbing removed — gone from both .env.schema files, both env.generated.ts, and turbo.json build.env; the fallbacks in apps/api/src/shared/utils.ts, apps/app/src/shared/utils.ts, and apps/app/src/features/auth/server/index.ts collapse to plain env reads.
  • Dev URLs rewritten to fixed ports.env.development for api/app/docs/web, plus apps/docs/src/shared/constants.ts and both Turbo generators, move from https://*.init.localhost to http://localhost:<port>.
  • Workflows and Studio dev scripts rewrittenpackages/workflows becomes inngest dev -u http://localhost:3000/workflows; packages/db drops --port ${PORT:-4983} and takes drizzle-kit's default.
  • Template tooling simplifiedscripts/template/portless.ts deleted, rename.ts no longer text-replaces <scope>.localhost, add.ts no longer calls restorePortlessWorkspaces.
  • Docs rewrittendocs/development.md, docs/getting-started.md, docs/generators.md, and docs/template-commands.md replace the Portless topology with a fixed-port table.

I verified the mechanical parts hold up: bunx oxlint scripts/ turbo/ reports 0 errors after the deletions, no dangling imports or unused exports remain, normalizedSourceScope in rename.ts is still live, no workspace has a studio script so removing that turbo.json task is safe, -u is the documented short form of --sdk-url with polling on by default (docs), and the ten stated ports match what each config actually produces with no collision against the Compose host ports (8000–8003).

ℹ️ The app and API auth instances no longer have isolated cookie jars

Cookies are scoped by host, never by port. apps/app (:3001, basePath /api/auth) and apps/api (:3000, basePath /auth) both run better-auth with the shared cookiePrefix: "init" from packages/auth/src/constants.ts, no domain, and the default path / — so they now write the same init.session_token into one jar on host localhost. The distinct app.init.localhost / api.init.localhost hostnames used to keep them apart. Because both dev configs share AUTH_SECRET and DATABASE_URL, the clobbered cookie still validates, so this is confusing rather than broken — but signing out on one signs the other out, and concurrent OAuth flows overwrite each other's state cookie.

Technical details
# Shared cookie jar between the two better-auth instances

## Affected sites
- `apps/api/.env.development:1``BASE_URL=http://localhost:3000`
- `apps/app/.env.development:1``PUBLIC_BASE_URL=http://localhost:3001`
- `packages/auth/src/constants.ts:6``AUTH_ADVANCED_OPTIONS.cookiePrefix = "init"`, shared by
  `apps/api/src/shared/auth.ts:16` and `apps/app/src/features/auth/server/index.ts:18`

## Required outcome
- A developer running `bun run dev` at the root and using both `:3000` and `:3001` in one browser
  profile should not have one server's session, cookie-cache, or OAuth-state cookie silently
  replace the other's — or, if that is acceptable for a template, the trade-off should be stated
  next to the one already documented in the PR description.

## Open questions for the human
- Are `apps/app` and `apps/api` intended to be run together, or are they alternative backends a
  user picks between during `bun template setup`? If they are alternatives, this is a non-issue
  and only worth a docs line.
- If they are meant to coexist, is a per-app `cookiePrefix` (rather than the single shared
  constant) the right lever, given `packages/auth` deliberately centralizes it today?

ℹ️ Nitpicks

  • packages/db/package.json:19drizzle-kit studio lost its ${PORT:-4983} override while packages/email and apps/mobile kept theirs. Studio's port is now the only one in the fleet that cannot be changed without editing the script.
  • packages/workflows/package.json:18 — the api's port is hardcoded here. Overriding PORT in apps/api/.env.local silently stops workflow syncing with no error at either end.
  • apps/docs/README.md:47-48 still says development "derives the matching web.<project>.localhost URL", but this PR changed DEVELOPMENT_MARKETING_URL in apps/docs/src/shared/constants.ts to a literal http://localhost:3006.
  • docs/plans/19-cross-platform-component-catalog.md:82 still refers to "the repository's normal Portless/Turbo conventions". Low impact — docs/plans is in init.cleanupPaths, so template consumers never receive it.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread docs/getting-started.md Outdated
Comment thread packages/workflows/package.json Outdated

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ One follow-up on the new 4000-block assignment.

Reviewed changes — delta since my review of b5ff345. The new commit dcd0ebb reassigns the three package dev servers into a sequential 4000 block.

  • Drizzle Studio 4983 → 4000packages/db/package.json:19 regains the ${PORT:-N} override that b5ff345 had dropped, now defaulting to 4000.
  • Email preview 4000 → 4001packages/email/package.json:15.
  • Inngest 8288 → 4002packages/workflows/package.json:18 adds --port ${PORT:-4002} alongside the existing -u flag, and apps/api/.env.development:17 follows with INNGEST_BASE_URL=http://localhost:4002.
  • Docs updated — the tables in docs/development.md and docs/getting-started.md now describe the 4000 block.

I checked the fallout: apps/api is the only workspace importing @init/workflows, so INNGEST_BASE_URL needed updating in exactly one place and it was; no 4983 or 8288 reference survives anywhere in the tree; and the three new ports do not collide with 3000–3006 or the Compose host ports. The ${PORT:-4983} regression I flagged on packages/db last round is resolved.

My earlier points on the shared localhost cookie jar, the .env.schema port claim in the docs, Inngest auto-discovery, and the hardcoded http://localhost:3000 in the workflows script all still apply unchanged — not repeated here.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread docs/development.md Outdated
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@adelrodriguez

Copy link
Copy Markdown
Collaborator Author

Addressed the review in 1c23875:

  • Shared cookie jar — real issue, fixed at the source: the api's better-auth instance now namespaces its cookies (init-api prefix) instead of sharing init with the app. The two instances never share a host in production, so the prefix split costs nothing there, and dev on localhost no longer clobbers sessions or OAuth state. The base prefix stays centralized in packages/auth.
  • Inngest auto-discovery — took the suggestion; --no-discovery added. Verified the dev server no longer logs discovery probes.
  • Bare local.drizzle.studio URL — kept Studio on 4000 (the sequential 4000-block is deliberate) and corrected every documented URL to https://local.drizzle.studio?port=4000.
  • .env.schema port claim — qualified in all three places: app workspaces declare PORT in .env.schema; the mobile server and package dev servers set theirs via ${PORT:-<port>} in their dev scripts. Chose qualification over adding schema entries because the shell expands those fallbacks before varlock loads, so a schema default would be documentation-only and misleading.
  • apps/docs/README.md — stale web.<project>.localhost derivation line updated to the http://localhost:3006 fallback.

Two deliberate non-changes: the hardcoded http://localhost:3000 in the workflows script is the api's documented fixed port — parameterizing it would reintroduce the indirection this PR removes; and the Portless mention in docs/plans/19 stays, since plans are historical records and the directory is cleanup-pathed out of template output.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes — delta since my review of dcd0ebb. The new commit 1c23875 is a pure review-feedback commit and closes every open point from the two prior rounds.

  • API auth cookies namespacedapps/api/src/shared/auth.ts:20 spreads AUTH_ADVANCED_OPTIONS and overrides cookiePrefix to `${AUTH_COOKIE_PREFIX}-api`, so the api writes init-api.* while apps/app keeps init.*.
  • Inngest auto-discovery disabledpackages/workflows/package.json:18 adds --no-discovery.
  • Drizzle Studio URLs correcteddocs/development.md:58,62 and docs/getting-started.md:97 now carry ?port=4000.
  • Port-source claim scopeddocs/development.md:47 and docs/getting-started.md:88,124 now say application workspaces declare PORT in .env.schema while the Mobile server and package dev servers set theirs in the dev script.
  • Docs README de-staledapps/docs/README.md:47-48 replaces web.<project>.localhost with the literal http://localhost:3006 that DEVELOPMENT_MARKETING_URL actually holds.

I verified each fix rather than taking it on trust. The cookie change is complete: better-auth@1.6.25/dist/cookies/index.mjs:26 funnels every cookie the instance issues through advanced.cookiePrefixsession_token, session_data, dont_remember (:47-50) and oauth_state (:205) — so the two jars on host localhost no longer overlap, and nothing in the repo hardcodes a cookie name that the rename would strand. --no-discovery is the documented flag and the same page confirms the 3000–3010 scan range that motivated it (docs). The qualified .env.schema wording now matches reality for all ten servers. oxfmt --check and oxlint are clean on the changed TypeScript.

ℹ️ The Expo client cannot see the api's cookies, before or after the rename

Not a regression — flagging it because this commit is the natural place a reader would assume mobile was covered too. packages/auth/src/expo/client.ts passes scheme and storagePrefix but never cookiePrefix, so @better-auth/expo falls back to "better-auth" and its startsWith gate rejects the api's cookies. That gate guards all cookie persistence, so the generated Expo client never stores a session. It failed the same way on init.*; the only thing this commit changes is that the eventual fix must use init-api, not AUTH_COOKIE_PREFIX.

Technical details
# Expo `cookiePrefix` is unset, so mobile never persists an api session

## Affected sites
- `packages/auth/src/expo/client.ts:4-10` — forwards `scheme` and `storagePrefix`, both
  `AUTH_COOKIE_PREFIX`, but not `cookiePrefix`
- `turbo/generators/commands/connect-backend.ts:534` — points the generated Expo client at
  `http://localhost:3000`, i.e. `apps/api`, which now issues `init-api.*`

## Evidence
`@better-auth/expo@1.6.25/dist/client.js`:

```js
const cookiePrefix = opts?.cookiePrefix || "better-auth";   // :269
// ...
if (hasBetterAuthCookies(setCookie, cookiePrefix)) {         // :327 — gates ALL persistence
```

`hasBetterAuthCookies` (`:174-186`) is a `startsWith(prefix)` test on each `Set-Cookie` name
after stripping `__Secure-`. Neither `init.session_token` nor `init-api.session_token`
matches `"better-auth"`, so `storage.setItem(cookieName, ...)` never runs and
`$sessionSignal` never fires. `getOAuthStateValue` (`:116-129`) reads
`<prefix>.oauth_state` under the same assumption.

## Required outcome
- The Expo client should recognize the cookies of whichever instance it is pointed at, or the
  gap should be tracked so it is not mistaken for fixed by this commit.

## Suggested approach (optional)
- Forward a `cookiePrefix` through `packages/auth/src/expo/client.ts`. The option accepts an
  array, so `[AUTH_COOKIE_PREFIX, \`${AUTH_COOKIE_PREFIX}-api\`]` covers both backends without
  the caller having to know which one it talks to.
- Do NOT rename `AUTH_COOKIE_PREFIX` itself to reach this: `packages/auth/src/expo/client.ts:6`
  reuses it as the deep-link `scheme`, which must keep matching `APP_ID` in
  `apps/mobile/app.config.js`. The api-side override in this commit correctly avoids that trap.

## Open questions for the human
- Is the Expo auth template exercised end to end anywhere, or is it copy-once scaffolding that
  a consumer is expected to finish? If the latter, a line in the generator output may be a
  better home for this than a code change.

Pullfrog  | View workflow run | Using Claude Opus𝕏

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