# Application URLs

> Give email, workers and HTTP routes one trusted application address. A URL resolver is not a redirect policy.

- Package: `@db3.ai/app/url`
- Canonical page: [https://db3.ai/docs/url](https://db3.ai/docs/url)
- Markdown: [https://db3.ai/docs/url.md](https://db3.ai/docs/url.md)
- Framework source of truth: `packages/app/src/url/README.md`

<a id="setup"></a>

## Configure the browser-facing URL

Set `new App({ url: { baseUrl: "https://your-app.example" } })`, or provide `APP_URL`. Resolution is explicit `baseUrl`, then `APP_URL`, then `PUBLIC_APP_URL`, then `localPort`. The frontend’s development port may differ from the backend port.

The service requires an HTTP(S) base or a valid local port. It does not read request Host headers. A configured environment base wins over the local fallback, so keep production values out of a local test shell.

- [Installation](https://db3.ai/docs/installation.md)
- [Validate configuration at boot](https://db3.ai/docs/config.md)

<a id="copy"></a>

## Copy the example

Use the independent application and tools from Installation. No SQL or running server is needed.

### Copy the shipped example

```bash
mkdir -p examples
cp -R node_modules/@db3.ai/app/src/url/examples/. examples/
```

<a id="run"></a>

## Run the links

Expect the relative path under `/workspace/`, the root-relative path under the origin, both rejection flags true, and the repaired sign-in URL.

### Run the lab

```bash
npx tsx examples/runApplicationLinks.ts
```

<a id="use"></a>

## Use the canonical generator

`app().url.baseUrl` is normalized without trailing slashes. `to("notes/one")` resolves beneath a configured base path; `to("/notes/one")` resolves from the origin root. Query strings and fragments follow the standard `URL` rules.

An absolute or protocol-relative argument can change origin. The example’s `applicationLink()` is an application-owned guard for root-relative same-origin redirects, not another framework method. Never treat `to()` as permission to fetch a URL or redirect a user.

### examples/runApplicationLinks.ts

```typescript
import { pathToFileURL } from 'node:url';
import { App } from '@db3.ai/app/server';
import { UrlGenerator } from '@db3.ai/app/url';

/**
 * Builds a link from an app-owned path, rejecting another origin.
 *
 * This is an example redirect policy, not a method supplied by UrlGenerator.
 * @param url - Trusted application URL generator.
 * @param path - Root-relative path to resolve.
 * @returns Same-origin absolute URL.
 */
export function applicationLink(url: UrlGenerator, path: string): string {
	if (!path.startsWith('/') || path.startsWith('//')) throw new Error('Use a root-relative application path.');
	const resolved = url.to(path);
	if (new URL(resolved).origin !== new URL(url.baseUrl).origin) throw new Error('Link must remain on the application origin.');
	return resolved;
}

/** Exercises canonical links, subpath resolution, invalid configuration and repair. */
export async function runApplicationLinks() {
	const application = new App({ url: { baseUrl: 'https://notes.example.test/workspace/' } });
	try {
		const url = application.url;
		let invalidBaseRejected = false;
		try { new UrlGenerator({ baseUrl: 'file:///private/notes' }); } catch { invalidBaseRejected = true; }
		let externalRejected = false;
		try { applicationLink(url, '//outside.example.test/'); } catch { externalRejected = true; }
		return { base: url.baseUrl, relative: url.to('notes/one'), root: applicationLink(url, '/notes/one'), externalRejected, invalidBaseRejected, repaired: new UrlGenerator({ baseUrl: 'https://notes.example.test' }).to('/sign-in') };
	} finally { await application.close(); }
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) console.log(JSON.stringify(await runApplicationLinks(), null, 2));
```

<a id="scope"></a>

## Keep request URLs separate

The canonical address is application configuration, not mutable per-request state. SSR receives each request URL on its own render context. Background jobs can use the same canonical service without an HTTP server.

There is no named-route registry, signed URL service, URL-shortener or remote-fetch allowlist here. Use explicit paths, and document any application-specific routing policy.

- [Server rendering](https://db3.ai/docs/ssr.md)

<a id="testing"></a>

## Testing

Create a `tests/url` directory and save the test below as `runApplicationLinks.test.ts`. It imports the example you copied into `examples/`. Keep the same folder layout so the relative import resolves.

Run from the application root with the development dependencies from Installation. These are consumer tests, not commands that assume a framework checkout.

### tests/url/runApplicationLinks.test.ts

```typescript
import { expect, it, vi } from 'vitest';
import { UrlGenerator } from '@db3.ai/app/url';
import { applicationLink, runApplicationLinks } from '../../examples/runApplicationLinks';

it('resolves trusted canonical links and repairs invalid configuration', async () => {
	expect(await runApplicationLinks()).toEqual({ base: 'https://notes.example.test/workspace', relative: 'https://notes.example.test/workspace/notes/one', root: 'https://notes.example.test/notes/one', externalRejected: true, invalidBaseRejected: true, repaired: 'https://notes.example.test/sign-in' });
	const url = new UrlGenerator({ baseUrl: 'https://notes.example.test' });
	for (const path of ['https://outside.example.test', '//outside.example.test', '/\\outside.example.test', 'javascript:alert(1)']) expect(() => applicationLink(url, path)).toThrow();
});

it('requires configuration and checks local ports when environment fallbacks are absent', () => {
	vi.stubEnv('APP_URL', ''); vi.stubEnv('PUBLIC_APP_URL', '');
	try {
		expect(() => new UrlGenerator()).toThrow('not configured');
		expect(() => new UrlGenerator({ localPort: 0 })).toThrow('local port');
		expect(new UrlGenerator({ localPort: 8717 }).baseUrl).toBe('http://localhost:8717');
	} finally { vi.unstubAllEnvs(); }
});
```

<a id="run-tests"></a>

## Run and extend the test

The tests clear URL environment fallbacks when checking missing configuration and restore them afterwards. Change the base subpath, test your redirect inputs and keep rejection of external origins.

### Run your copied test and check types

```bash
npx vitest run tests/url/runApplicationLinks.test.ts
npx tsc --noEmit --target ES2022 --module ESNext --moduleResolution Bundler --types node --skipLibCheck examples/*.ts
```

<a id="coverage"></a>

## Coverage and API

Tested: explicit base, subpaths, local fallback, invalid/missing configuration, guarded external references and recovery. Reference: the two configuration options and complete generator. No external service is contacted.

- [URL API](https://db3.ai/docs/url-api.md)

## Behavioural verification
Builds canonical links, checks root versus relative paths, rejects invalid config and external redirects, then repairs the base.
- Behaviour test: `packages/app/src/url/tests/examples/runApplicationLinks.test.ts`
- Repository test command (framework checkout only): `npm run test:service --workspace @platform/app -- url --maxWorkers=1`
- In an installed application, use the walkthrough commands instead of this repository test.
- Expected outcome: The guide test passes against the real framework components.
- Environment: Node.js only; no HTTP requests, database or external provider.

## Related documentation
- [Configuration](https://db3.ai/docs/config.md): Read settings once at boot. Parse environment values, validate the bits your application needs and pass them into the services that use them.
- [Mail](https://db3.ai/docs/mail.md): Build and preview an application email locally, then select a transport when you are ready to send it.
- [Render public HTML](https://db3.ai/docs/ssr.md): SSR owns the document and request boundary. Your application owns the pages, router, data and client hydration.

## Framework-owned source: `packages/app/src/url/README.md`

This is the exact source document captured by the documentation build. Use it for detailed API and workflow guidance, subject to the public package exports and behavioural evidence identified above.

````markdown
# Application URLs

## Runnable links and tests

Copy the installed package's `src/url/examples/` into `examples/`, then run
`npx tsx examples/runApplicationLinks.ts`. No SQL or network is needed. The lab
checks base/subpath resolution, invalid configuration, an application-owned
same-origin redirect guard and recovery. Copy the exact
`tests/examples/runApplicationLinks.test.ts` into `tests/url/` to extend it.
`to()` follows standard URL resolution and permits absolute references; it is
not an open-redirect or remote-fetch protection mechanism.

`app().url` is the framework-owned source for the canonical browser-facing
application URL. It is deliberately separate from a concrete server such as
Fastify so URL generation also works in queue workers, schedulers, console
commands, and server-side rendering processes.

Configure a deployed application with `APP_URL` or an explicit `baseUrl`. Apps
with a separate frontend development server can pass its configured local port:

```ts
const application = new App({
	url: {
		localPort: 8888,
	},
});

application.url.baseUrl;
// http://localhost:8888

application.url.to('/api/oauth/callback');
// http://localhost:8888/api/oauth/callback
```

Application code should use this service instead of reading request host
headers or reconstructing origins from environment variables. Request headers
are untrusted and a concrete HTTP server may not exist in background processes.

Future SSR request URLs should be carried by `app().requestContext`; they must
not mutate the canonical URL used for OAuth callbacks, email links, billing
returns, publishing, and background jobs.
````

## Guidance for AI tools
Use the documented public import `@db3.ai/app/url` and its exported types. Prefer the source-backed examples and behavioural outcomes above over invented APIs or source-relative internal imports.
