Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions .certs/.gitignore

This file was deleted.

2 changes: 1 addition & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"lint": "biome lint",
"clean": "rimraf dist",
"prestart": "pnpm run clean && pnpm run build",
"start": "func start --typescript",
"start": "portless data-access.sharethrift.localhost node start-dev.mjs",
"azurite": "azurite-blob --silent --location ../../__blobstorage__ & azurite-queue --silent --location ../../__queuestorage__ & azurite-table --silent --location ../../__tablestorage__"
},
"dependencies": {
Expand Down
16 changes: 16 additions & 0 deletions apps/api/start-dev.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { spawn } from 'node:child_process';

const envPort = process.env.PORT;

if (!envPort) {
console.error(
'PORT environment variable is not set. Ensure portless (or your dev environment) is running and has injected a port.',
);
process.exit(1);
}

const port = envPort;
const child = spawn('func', ['start', '--typescript', '--port', port], { stdio: 'inherit' });
child.on('exit', (code, signal) => {
process.exitCode = signal ? 1 : (code ?? 1);
});
75 changes: 75 additions & 0 deletions apps/docs/docs/decisions/0025-portless-local-https.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
sidebar_position: 25
sidebar_label: 0025 Portless Local HTTPS
description: "Decision record for replacing mkcert + manual HTTPS proxy with portless for local development."
status:
date: 2026-02-26
deciders:
---

# Local HTTPS Development: portless vs mkcert + manual proxy

## Context and Problem Statement

Local development benefits from HTTPS with named subdomains (e.g. `*.sharethrift.localhost`) to accurately mirror production behaviour: OAuth redirect URIs, CORS policies, cookies, and AI agents all depend on a consistent HTTPS origin. Without this, the local environment diverges from production in ways that are hard to detect until deployment.

The original solution used `mkcert` to generate a wildcard certificate stored in `.certs/`, combined with a hand-written `local-https-proxy.js` to front Azure Functions, necessary due to `func start`'s broken `--cert` flag in v4.2.2. Each service ran on its own numbered port, requiring developers to remember and configure many separate port assignments. A solution is needed that is easier to maintain and reduces the mental load for developers.

## Decision Drivers

- Developers should not need to manually manage TLS certificates
- All local services should be accessible via consistent, named HTTPS URLs
- The approach must not affect production builds or CI pipelines
- Multiple ports across services makes `.env` configuration error-prone
- AI Agents should be able to work on the subdomains

## Considered Options

- **portless** - globally installed reverse proxy daemon that maps subdomains to local ports with auto-trusted TLS certificates
- **mkcert + manual HTTPS proxy** - existing approach using a wildcard cert and a custom Node.js HTTPS proxy


### Consequences - portless

**Positive**

- No certificate management; TLS certs are auto-generated and auto-trusted
- Single port (`1355`) for all services: `.env` and `local.settings.json` are simpler
- Subdomain names in URLs (`data-access`, `mock-auth`, etc.) make it immediately obvious which service is being called
- `local-https-proxy.js` is deleted, now one less script to maintain
- Removes compatibility issues with Azure Functions' `--cert` flag

**Negative**

- Requires a one-time global install: `pnpm install -g portless`
- `func start` and Docusaurus do not respect the `PORT` environment variable injected by portless; thin `start-dev.mjs` wrapper scripts are required to read `process.env.PORT` and pass `--port` explicitly
- On macOS, bare `localhost` resolves to `::1` (IPv6), but portless connects via `127.0.0.1` (IPv4); Docusaurus must be started with `--host 127.0.0.1` to avoid a Bad Gateway error
- `portless proxy start --https` silently no-ops if the proxy is already running in HTTP mode; a `dev-cleanup.mjs` script is needed to run `portless proxy stop` and kill zombie processes before each dev session

### Consequences - mkcert

**Positive**

- No global tooling dependency and certificates live in the repo (gitignored)
- Works with any port assignment without daemon management

**Negative**

- Developers need to run `mkcert -install` and `mkcert` on each machine
- Certificate files require explicit exclusion from version control
- `local-https-proxy.js` must be kept in sync with Azure Functions behaviour
- CI required `fs.existsSync()` guards to skip HTTPS when certs are absent
- Many different port numbers to configure across `.env`, `local.settings.json`, and docs

## Decision Outcome

Chosen option: **portless**

`portless` eliminates the entire certificate lifecycle (generation, installation, `.gitignore` entries, CI detection guards) and replaces the custom `local-https-proxy.js` with a zero-config daemon. All services become reachable via `https://<name>.sharethrift.localhost:1355`, a single consistent pattern. The old multi-port layout is replaced by one port and named subdomains that will remain standard throughout the developer lifecycle.

## More Information

- [portless on npm](https://www.npmjs.com/package/portless)
- [portless docs](https://port1355.dev/)
- [mkcert repo](https://github.com/FiloSottile/mkcert)
- `apps/api/start-dev.mjs`, `apps/docs/start-dev.mjs` — wrapper scripts that pass `PORT` to tools that require an explicit `--port` flag
133 changes: 0 additions & 133 deletions apps/docs/docs/technical-overview/localhost-subdomain-setup.md

This file was deleted.

32 changes: 0 additions & 32 deletions apps/docs/docusaurus.config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { themes as prismThemes } from 'prism-react-renderer';
import type { Config } from '@docusaurus/types';
import type * as Preset from '@docusaurus/preset-classic';
import path from 'node:path';
import fs from 'node:fs';

// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)

Expand Down Expand Up @@ -158,36 +156,6 @@ const config: Config = {
darkTheme: prismThemes.dracula,
},
} satisfies Preset.ThemeConfig,

// Custom webpack configuration for HTTPS dev server
plugins: [
function httpsPlugin() {
return {
name: 'https-plugin',
configureWebpack() {
const workspaceRoot = path.resolve(__dirname, '../../');
const certKeyPath = path.join(workspaceRoot, '.certs/sharethrift.localhost-key.pem');
const certPath = path.join(workspaceRoot, '.certs/sharethrift.localhost.pem');
const hasCerts = fs.existsSync(certKeyPath) && fs.existsSync(certPath);

if (hasCerts) {
return {
devServer: {
server: {
type: 'https',
options: {
key: fs.readFileSync(certKeyPath),
cert: fs.readFileSync(certPath),
},
},
},
};
}
return {};
},
};
},
],
};

export default config;
2 changes: 1 addition & 1 deletion apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start --port 3002 --host docs.sharethrift.localhost --no-open",
"start": "portless docs.sharethrift.localhost node start-dev.mjs",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
Expand Down
18 changes: 18 additions & 0 deletions apps/docs/start-dev.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { spawn } from 'node:child_process';

const envPort = process.env.PORT;

if (!envPort) {
console.error(
'PORT environment variable is not set. Ensure portless (or your dev environment) is running and has injected a port.',
);
process.exit(1);
}

const port = envPort;
// Use 127.0.0.1 explicitly to ensure IPv4 binding — portless proxy connects via IPv4,
// but Node.js may resolve 'localhost' to ::1 (IPv6) on macOS, causing Bad Gateway.
const child = spawn('pnpm', ['exec', 'docusaurus', 'start', '--host', '127.0.0.1', '--port', port, '--no-open'], { stdio: 'inherit' });
child.on('exit', (code, signal) => {
process.exitCode = signal ? 1 : (code ?? 1);
});
6 changes: 4 additions & 2 deletions apps/server-messaging-mock/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
"prebuild": "biome lint",
"build": "tsc --build",
"clean": "rimraf dist",
"start": "node -r dotenv/config dist/src/index.js",
"dev": "tsc-watch --onSuccess \"node -r dotenv/config dist/src/index.js\""
"start": "portless mock-messaging.sharethrift.localhost node -r dotenv/config dist/src/index.js",
"dev": "tsc-watch --onSuccess \"node -r dotenv/config dist/src/index.js\"",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"@cellix/server-messaging-seedwork": "workspace:*",
Expand Down
17 changes: 0 additions & 17 deletions apps/server-messaging-mock/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import dotenv from 'dotenv';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
startMockMessagingServer,
type MockMessagingServerConfig,
Expand All @@ -17,27 +15,12 @@ const setupEnvironment = () => {

setupEnvironment();

// Detect certificate availability to determine protocol
const projectRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'../../../../',
);
const certKeyPath = path.join(projectRoot, '.certs/sharethrift.localhost-key.pem');
const certPath = path.join(projectRoot, '.certs/sharethrift.localhost.pem');

// biome-ignore lint: using bracket notation for environment variable access
const port = Number(process.env['PORT'] ?? 10000);

const fs = await import('node:fs');
const hasCerts = fs.existsSync(certKeyPath) && fs.existsSync(certPath);

const config: MockMessagingServerConfig = {
port,
useHttps: hasCerts,
seedData: true,
host: hasCerts ? 'mock-messaging.sharethrift.localhost' : 'localhost',
certKeyPath,
certPath,
seedMockData,
};

Expand Down
4 changes: 2 additions & 2 deletions apps/server-oauth2-mock/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
"clean": "rimraf dist node_modules tsconfig.tsbuildinfo && tsc --build --clean",
"lint": "biome lint",
"format": "biome format --write",
"start": "node dist/src/index.js",
"dev": "tsx watch src/index.ts"
"start": "portless mock-auth.sharethrift.localhost node dist/src/index.js",
"dev": "portless mock-auth.sharethrift.localhost tsx watch src/index.ts"
},
"dependencies": {
"@cellix/server-oauth2-seedwork": "workspace:*",
Expand Down
Loading