Local Portless Workspace

Develop mf-dev packages against the main Marketfuel apps without publishing packages first.

Overview

Setup creates an outer pnpm workspace with detached git worktrees for mf-dev, mf_admin, mf_store, mf_ui_theme, and mf_api_server.

Portless gives each service a stable local HTTPS URL while the apps continue to run on internal ports managed by portless.

ServiceDefault URLNamespaced example
APIhttps://api.mf.localhosthttps://api-mfd-1234.mf.localhost
Adminhttps://admin.mf.localhosthttps://admin-mfd-1234.mf.localhost
Storehttps://store.mf.localhosthttps://store-mfd-1234.mf.localhost
Theme docshttps://theme.mf.localhosthttps://theme-mfd-1234.mf.localhost
mf-dev docshttps://docs.mf.localhosthttps://docs-mfd-1234.mf.localhost

Passing a namespace like MFD-1234 changes both the workspace directory and the hostnames. Without a namespace, setup creates ../marketfuel-local and uses the default URLs. With a namespace, setup creates ../MFD-1234 and prefixes each service hostname with a DNS-safe version of that value.

Prerequisites

Node.js 20+

Node runs the docs app, React apps, pnpm, Turbo, and portless. Use Node 20 or newer so every workspace package uses the same runtime family as CI.

node --version # Fresh macOS install with nvm curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash nvm install 20 nvm use 20 node --version

pnpm 9

pnpm installs the outer workspace and links local @branditdev/* packages into the apps without publishing them first.

pnpm --version # Fresh install through Corepack corepack enable corepack prepare pnpm@9.0.0 --activate pnpm --version

Ruby and Bundler

Ruby and Bundler run mf_api_server and install its Rails dependencies.

ruby --version bundle --version # Fresh macOS install with rbenv brew install rbenv ruby-build cd ~/code/mf_api_server rbenv install "$(cat .ruby-version)" rbenv local "$(cat .ruby-version)" gem install bundler ruby --version bundle --version

Foreman

Foreman reads generated Procfile.portless.* profiles and starts only the process group you choose, such as core, frontend, packages, docs, or full.

foreman --version # Fresh install gem install foreman foreman --version

portless

portless replaces hardcoded local ports with stable HTTPS URLs like https://admin.mf.localhost. Setup installs portless into the generated outer workspace and the Procfile calls it with pnpm exec portless. A global install is still useful for one-off commands like portless list.

portless --version # Optional fresh global install for one-off commands npm install -g portless portless --version # First-time HTTPS trust setup, if prompted later portless trust

Package registry tokens

The React apps depend on private packages from GitHub Packages and Font Awesome packages from the Font Awesome registry. The generated outer .npmrc reads tokens from your shell environment.

# Verify tokens are available to pnpm test -n "$GITHUB_TOKEN" && echo "GITHUB_TOKEN is set" test -n "$FONT_AWESOME_TOKEN" && echo "FONT_AWESOME_TOKEN is set" # Add these to your shell profile if needed export GITHUB_TOKEN="<github-token-with-packages-read-access>" export FONT_AWESOME_TOKEN="<font-awesome-token>"

Create the workspace

Run the setup command from your mf-dev checkout.

pnpm setup:local -- MFD-1234

By default, setup reads source checkouts from the sibling directory of mf-dev, uses the positional argument as the full workspace directory name and URL namespace, and uses each source checkout's current HEAD. Omitting the argument creates ../marketfuel-local and uses unprefixed URLs like https://admin.mf.localhost.

Customize refs or paths

ROOT=../custom-workspace SOURCE_ROOT=.. MF_DEV_REF=my-mf-dev-branch MF_ADMIN_REF=main MF_STORE_REF=main MF_UI_THEME_REF=main MF_API_REF=main pnpm setup:local

Pass --force to overwrite generated workspace files without recreating existing worktrees.

pnpm setup:local -- MFD-1234 --force

The generated root package.json forces @branditdev/mf_ui_theme to the local mf_ui_theme workspace path and @branditdev/mf-types to mf_api_server/packages/mf-types. This keeps admin and store pointed at local package changes without publishing new package versions.

URL namespaces

The setup argument controls both the generated directory name and URL namespace. pnpm setup:local creates ../marketfuel-local and uses unprefixed URLs. pnpm setup:local -- MFD-1234 creates ../MFD-1234 and uses URLs like https://admin-mfd-1234.mf.localhost.

Namespaces are normalized for hostnames. For example, MFD_1234 / Checkout becomes mfd-1234-checkout, so the admin URL becomes https://admin-mfd-1234-checkout.mf.localhost. Only open URLs for services you have started: the default pnpm dev starts API, admin, and store; docs and theme require pnpm dev:docs, pnpm dev:theme, or pnpm dev:full.

Install and run

cd ../MFD-1234 pnpm install pnpm dev

The generated pnpm dev command aliases pnpm dev:core. It builds linked packages, starts the portless proxy, then runs foreman start -f Procfile.portless.core for the API, admin, and store only.

pnpm dev:api # Rails API only pnpm dev:admin # Admin only pnpm dev:store # Store only pnpm dev:frontend # Admin + store pnpm dev:packages # Optional hooks/utils/ui/theme package watchers pnpm dev:worker # Sidekiq only pnpm dev:docs # mf-dev docs only pnpm dev:theme # mf_ui_theme app + theme build watcher pnpm dev:full # Everything

The initial linked package build is required because the linked packages export files from dist, and fresh worktrees do not have those gitignored build outputs yet. Watch processes for @branditdev/hooks, @branditdev/utils, @branditdev/ui, and @branditdev/mf_ui_theme are opt-in through pnpm dev:packagesor pnpm dev:full.

Starting the proxy on HTTPS port 443 may ask for your macOS password once. Starting it before Foreman avoids every app process asking for sudo at the same time.

The generated Procfile uses pnpm exec portless so Foreman does not need your shell's global npm bin on its PATH. For Rails and Sidekiq, it also sets RBENV_VERSION from mf_api_server/.ruby-version and runs commands through rbenv exec. Rails reads the portless-assigned PORT directly, so the generated API command does not pass -p. The API and worker also get a local REDIS_URL=redis://localhost:6379/0 default.

Because credentials and env files are gitignored, setup links local-only files from your source checkouts into the generated worktrees when they exist locally: mf_api_server/config/master.key, mf_api_server/.env, mf_admin/.env, and mf_store/.env. If any are missing, ask the team for the local development values.

If you see warnings that package-level pnpm fields will not take effect, that is expected for this outer workspace. pnpm only applies overrides from the workspace root, so setup writes the shared app and theme overrides into the generated root package.json.

A 404 for a private package usually means GITHUB_TOKEN is missing or does not have package read access, not that the package is public npm-missing.

Cleanup

If setup fails because a worktree is missing but already registered, or if you want to remove the generated workspace, run the cleanup command from your mf-dev checkout.

pnpm cleanup:local

Use the same workspace name, WORKSPACE_PREFIX, ROOT, or SOURCE_ROOT values you used when creating the workspace.

pnpm cleanup:local -- MFD-1234

Frontend API URLs

Setup links real frontend env files from your source checkouts when available. If they are missing, it writes minimal fallback files. Keep API URLs pointed at the portless API URL for one-off app starts. In namespaced workspaces, replace the default host with the namespaced host, such as api-mfd-1234.mf.localhost.

mf_admin/.env

PORT=3500 HOST=localhost WDS_SOCKET_PORT=0 ALLOWED_HOSTS=admin-mfd-1234.mf.localhost, localhost, 127.0.0.1 REACT_APP_BACKEND_API_URL=https://api-mfd-1234.mf.localhost/api/v1 REACT_APP_ENV=development

mf_store/.env

PORT=3501 WDS_SOCKET_PORT=0 REACT_APP_BACKEND_API_URL=https://api-mfd-1234.mf.localhost/api/v1/portal_core

Troubleshooting

  • Check active routes with portless list.
  • Remove dead routes with portless prune.
  • If Safari cannot resolve *.localhost, run portless hosts sync.
  • Reset portless state with portless clean.