Portals
Portals are authenticated URLs that expose web servers running inside an orb, so you can open a dev server or preview app straight from the thread’s Portal tab.

You can navigate in portals just like in browser. And you can also annotate what you see and send your comments directly to the agent:
The easiest way to get started is to ask Amp:
Put Each Responsibility in the Right File
| File | Purpose |
|---|---|
.agents/setup | Prepare a new orb by installing dependencies, generating files, and checking required tools |
.agents/resume | Perform optional, quick authentication or repair work after activation and each wake |
.amp/services.yaml | Declare long-running services and their portal links |
.amp/portals/*.json | Store generated portal links for the current orb and thread |
Use .agents/setup for work a fresh orb needs before an agent starts. Make the script executable
and safe to run more than once:
#!/usr/bin/env bash
set -euo pipefail
npm ci chmod +x .agents/setup Do not start a development server or run amp orb services ensure from .agents/setup. Setup
prepares the environment. .amp/services.yaml owns the lifecycle of long-running services.
Most repositories do not need .agents/resume for portals. Add it only for quick, idempotent
repair work that Amp cannot supervise as a service, such as reconnecting a tunnel. Amp starts this
hook after initial orb activation and whenever the orb wakes. Amp waits for up to 10 seconds before
it lets the agent continue. Do not install dependencies or restart declared portal services in
this hook.
Read Customizing Orbs for complete setup and resume examples and their log locations.
Amp generates .amp/portals/*.json when it ensures a service. Edit .amp/services.yaml, not the
generated file, when you want to change a declared service or its links. Ignore generated portal
state in Git:
.amp/portals/ Define Services
Commit .amp/services.yaml so every orb knows how to run the repository’s development services:
services:
web:
command: pnpm dev -- --host 0.0.0.0 --port "$PORT"
cwd: app
health: /healthz
env:
API_MODE: development
review: true
agent: control
portal:
url: /
title: App
description: Use the seeded development account.
portals:
- url: /admin
title: Admin
description: Open the administration tools. The file needs a nonempty top-level services mapping. Service names can contain lowercase letters,
numbers, and hyphens. A name must start with a letter or number and can have up to 32 characters.
Each service supports these fields:
| Field | Purpose |
|---|---|
command | Required shell command. It runs through a login shell and must listen on $PORT. |
cwd | Working directory relative to the repository root. It defaults to the repository root. |
port | Fixed local port from 1 to 65535. Omit it to let Amp choose a free port that stays the same across restarts. |
env | String environment variables for the command. Values can refer to another service’s public URL. |
health | HTTP path Amp requests after the port opens. HTTP 2xx and 3xx mean ready. |
portal | Primary portal link. Use true for / with the service name, or provide link details. |
portals | More portal links for the same service. Each entry needs url and title. |
review | Whether Amp injects the review widget. It defaults to true. |
agent | Agent access to the portal. Use off, observe, or control. It defaults to off. |
A portal url can be a path that starts with / or an absolute HTTP or HTTPS URL. A primary portal mapping defaults to / and the service name when you omit url or title.
Amp manages PORT, PUBLIC_URL, and AMP_THREAD_ID, so you cannot set them inside env. Every
service receives PORT and AMP_THREAD_ID. A service with at least one portal link also receives PUBLIC_URL.
$AMP_USER_EMAIL is the only placeholder supported in a portal entry. Amp replaces it in the url, title, and description. It URL-encodes the value when it appears in url.
Start Declared Services
Run every declared service from the repository root:
amp orb services ensure Ensure reads .amp/services.yaml, checks each service, starts anything missing, and waits for
readiness. It then writes the Portal tab state and prints the exact portal URLs. The Portal tab also
runs ensure when you open a declared portal that has not started yet.
Use the printed URL. Do not use a raw localhost URL, construct a portal hostname, or save one in
configuration. When an application needs its public origin, read PUBLIC_URL at runtime.
For scripts that need structured service status, run:
amp orb services ensure --json Add Health Checks
Without health, Amp considers a service ready when its port accepts a connection. Set health when the application needs more time after opening the port:
services:
web:
command: pnpm dev -- --host 0.0.0.0 --port "$PORT"
health: /healthz
portal: true Amp sends a GET request to the path. HTTP 2xx and 3xx responses pass the check. A configured portal URL can exist even when readiness fails, so the URL alone does not prove that the app responded.
Connect Declared Services
One service can use another service’s final public URL through an env reference:
services:
api:
command: go run ./cmd/api
env:
FRONTEND_URL: ${services.web.publicURL}
web:
command: pnpm dev -- --host 0.0.0.0 --port "$PORT"
portal: true Amp starts the referenced service first and replaces the reference with its final URL. The referenced service must have a portal. References to missing services and dependency cycles are rejected.
For separate frontend and API services, expose the frontend and route API requests through the frontend development server’s proxy. Browser requests between two portal hosts are not supported because CORS preflight requests do not carry portal authentication.
Use a folder to group related links while leaving other links at the top level:
services:
web:
command: pnpm dev
portal: true
portals:
- folder: Marketing site
links:
- url: /home
title: Homepage
- url: /pricing
title: Pricing
- url: /storybook
title: Storybook amp orb services ensure also writes the Portal tab’s links to .amp/portals/*.json. To add
custom links, write a separate manifest by hand. Service-generated manifests are an implementation
detail, so edit .amp/services.yaml instead of editing them. Get each custom URL from amp orb portal 3000 (pass the local port) instead of hardcoding the domain format:
{
"links": [
{
"label": "App",
"url": "https://t-…-p3000.onamp.dev",
"note": "Use the seeded test account."
},
{
"folder": "Admin",
"links": [
{ "label": "Users", "url": "https://t-…-p3000.onamp.dev/admin/users" },
{ "label": "Settings", "url": "https://t-…-p3000.onamp.dev/admin/settings" }
]
}
]
} A link’s optional note is shown as an info popover next to it. A folder can contain links one
level deep, and each folder name must be unique within a service. The Portal tab shows the folder
as a menu and keeps other links at the top level.
Manage Services
Use these commands to inspect and control supervised services:
amp orb service list
amp orb service status web
amp orb service logs web
amp orb service restart web
amp orb service stop web Restart a declared service after changing its command, dependencies, or source when the development
server does not reload that change itself. Restart reads the latest .amp/services.yaml and keeps
the service’s port and portal configuration.
Do not use nohup, setsid, a background &, or tmux to keep a portal server alive. Processes
started that way can be stopped when Amp updates or the orb resumes. Use a supervised service.
Start a One-Off Service
Use amp orb service start for a service needed only in the current thread:
amp orb service start app \
--command 'npm run preview -- --host 0.0.0.0 --port "$PORT"' \
--portal \
--title 'App' \
--description 'Use the seeded development account.' The outer quotes keep the shell from expanding $PORT before Amp supplies it. You can also pass --cwd or --port. With --portal, Amp waits for the listener, detects HTTP or HTTPS, writes .amp/portals/<name>.json, and prints the URL.
Amp waits up to 60 seconds for that port, so a hardcoded wrong port causes a long wait followed by NOT RESPONDING and exit status 1. The reported port is the app’s internal port. The p<number> in
the public URL may differ because it identifies Amp’s Portal proxy. A manifest and URL may still be
printed after a readiness failure. They mean the link was configured, not that the app responded.
Check amp orb service logs <name>.
The start command rejects a name already declared in .amp/services.yaml. Use ensure or restart
for declared services. If a one-off service will be useful in future orbs, move its configuration
into .amp/services.yaml.
amp orb portal <port> is a lower-level command for a server that is already listening. It creates
or prints a portal URL, but it does not start or supervise the server. Prefer declared services for
repository development servers.
Choose a Portal Hostname
Portal URLs are generated subdomains of onamp.dev by default. To give one portal a specific
hostname, run amp orb portal <port> --hostname <hostname> with a managed hostname like <name>--<namespace>.onamp.dev or a hostname on a custom domain from your Domains settings
page. The hostname applies only to that port.
A workspace can also select Make Default for Portals on an active domain on its Domains page.
New portal links for the workspace’s threads then use subdomains of that domain, such as t-<thread>-p<port>.portal.example.com, so your people only need to trust links on your own
domain. Existing URLs keep working.
Reuse a Custom Hostname
A custom hostname stays assigned when you archive its thread. To move it to a portal in another thread, run this command inside the new thread’s orb:
amp orb portal <port> --hostname <hostname> --take-over Amp moves the hostname from the other thread portal to the current portal. You can take over only a
hostname that the new thread can use. Without --take-over, Amp reports that the hostname is
already in use.
Review and Comment
Open the portal from the thread’s Portal tab. The floating review button lets you select an element and send a comment to the thread. Amp receives your comment with the selected text and page location. It also receives the source location when the app can provide one.
Portal comments work well for visual fixes because you can point at the exact part of the running app instead of describing its location.
The review widget is enabled by default. Disable it for every page in one service with:
services:
web:
command: pnpm dev -- --host 0.0.0.0 --port "$PORT"
review: false
portal: true Disable it for one response with this header:
x-amp-review-widget: off Set agent: observe to let the agent read browser logs and errors. Set agent: control to also let
the agent request page inspection or control. A person in the portal tab must still approve control.
Disable agent access for one response with x-amp-portal-agent: off.
Link Back to Amp
Use target="_amp" on a link to let a person leave the Portal pane and open an Amp page in the
containing Amp app:
<a href="https://ampcode.com/threads/T-..." target="_amp">Open the thread in Amp</a> Handle Development Sign-In
Vite
Amp sets AMP_ORB=1 inside every orb. When that variable is set, configure the dev server to
accept requests for any host because each portal has a generated hostname. Vite uses true rather
than '*' for this setting:
export default defineConfig({
server: {
allowedHosts: process.env.AMP_ORB ? true : undefined,
},
}) Keep the existing configuration and plugins when adding this setting. Do not allow every host outside an orb.
Laravel
Start Laravel from .amp/services.yaml, where $PUBLIC_URL is available. Assign it at runtime;
never save a generated portal hostname:
services:
web:
command: >-
php artisan optimize:clear &&
APP_URL="$PUBLIC_URL" ASSET_URL="$PUBLIC_URL"
php artisan serve --host=0.0.0.0 --port="$PORT"
portal: true Amp supplies the public Portal Host and standard forwarded HTTPS headers. Use Laravel’s normal
trusted-proxy handling; do not inspect provider-specific headers or replace Request::capture().
Clear Laravel configuration and any full-response cache before listening whenever the public
origin changes.
Dev Magic Link Authentication
OAuth, email links, and password managers make repeated development sign-in difficult for both agents and reviewers. Add a development-only route such as:
GET /__dev/log-me-in/<email>?returnTo=<path> The route should find the user with the given email address, or create the user if absent. Create a
session and set the application’s normal session cookie, using the same cookie name and options as
the real sign-in flow. Redirect to a relative path on the same origin. Reject absolute and
protocol-relative return URLs to prevent open redirects. If returnTo is missing or not safe,
redirect to /. Return 404 outside local development by checking both the framework development
flag and that NODE_ENV is not production. Do not include the route in production builds.
Put an example URL and test account in AGENTS.md so agents can discover them. A request made with curl does not sign in the browser, so open the URL in the browser session used for testing.
You can make the development login the primary portal link without hardcoding an email:
services:
web:
command: pnpm dev -- --host 0.0.0.0 --port "$PORT"
portal:
url: /__dev/log-me-in/$AMP_USER_EMAIL?returnTo=/
title: App
description: Sign in as $AMP_USER_EMAIL. Access and Lifecycle
Portal access is separate from application authentication. By default, only people who can view the thread can access its portals. A multiplayer participant can use the portal for as long as their access to that thread remains active.
To share a portal with anyone, open Portal Options, select Make Public, and choose 1 hour, 3 hours, 1 day, or 7 days. Anyone with the link can open the portal during that time without signing in to Amp. Public access applies only to that port. The public link does not grant access to the thread or its review tools.
Amp sets X-Amp-Authenticated on requests sent through the external portal proxy. The header includes the following tokens, separated by commas:
amp-user=yes|nosays whether the visitor is a registered Amp user. The user may be outside the workspace.workspace-member=yes|nosays whether the user belongs to the workspace that owns the thread.collaborator=yes|noisyesfor the thread creator. For a multiplayer thread, it is alsoyesfor anyone who can currently contribute to the thread.
The possible values are:
- Unauthenticated visitor:
amp-user=no, workspace-member=no, collaborator=no - Amp user outside the workspace:
amp-user=yes, workspace-member=no, collaborator=no - Creator of a personal thread:
amp-user=yes, workspace-member=no, collaborator=yes - Workspace member:
amp-user=yes, workspace-member=yes, collaborator=no - Workspace member for a portal of a multiplayer thread who can currently contribute:
amp-user=yes, workspace-member=yes, collaborator=yes
In most cases, you’ll just want to check for string-includes workspace-member=yes to perform access control.
Amp also sends these user identification headers when the user is a workspace member or collaborator:
X-Amp-User-ID: user_abcd1234
X-Amp-User-Display-Name: Ada Lovelace
X-Amp-User-Email: ada@example.com
X-Amp-User-URL: https://ampcode.com/@ada Do not add access to .amp/services.yaml. The current format does not support it. Amp manages
portal access separately from the service definition.
Opening a portal wakes a paused orb. The orb resumes billing while the request and services are active, then automatically pauses again after five minutes without activity. Archiving the thread also pauses its orb.
Inside the owning orb, requests to the thread’s portal URL go directly to the local service. This
means browser automation, curl, Node.js, and Python can use PUBLIC_URL without a second portal
sign-in. This bypasses only Amp’s portal authentication. It does not bypass authentication in the
application itself, and it does not test the external viewer sign-in flow. The app receives the
same public Host and standard forwarded HTTPS headers as through the external portal path.
Troubleshooting
Vite Rejects the Portal Hostname
If Vite returns Blocked request. This host (...) is not allowed, merge the Orb-only server.allowedHosts setting shown above into the user’s existing vite.config.js. Keep all
existing config and plugins.
Keep the service’s fixed port in .amp/services.yaml. After changing the Vite config, restart the
service or run amp orb services ensure, then test through the actual Portal hostname. To check
Vite directly, curl the local service with the Portal hostname in the Host header.
First rerun ensure. It repairs a missing supervised service and its portal without starting a duplicate:
amp orb services ensure If the service does not become ready, inspect its status and logs:
amp orb service status <name>
amp orb service logs <name> Check these common causes:
- The command listens on a hardcoded port instead of
$PORT. - The process exits after starting a child server.
- The configured health path returns an error or never becomes ready.
- The development server rejects the generated portal hostname.
- The app expects its own login, but the browser opened only the portal URL.
A printed portal URL means Amp configured the public route. It does not prove that the service
passed its readiness check. After fixing the command or configuration, run amp orb service restart <name> and open the exact URL printed by Amp.