External interfaces
Port already gives you more than one way to work with your organization's context lake, starting with the Port web app itself: pages, dashboards, and widgets like plugins, which you host inside Port.
External interfaces extend this flexibility: your own app, hosted outside Port, reading and acting on that data as the person using it.
Why build on Port
External interfaces let you bring Port into your own systems, surfacing context lake data, triggering workflows, and calling other Port tools from an app you build and control. That's also useful when you want to visualize your context lake in a way Port's own interface doesn't cover today, a different look and feel, or an experience embedded inside another product. Either way, it runs on Port's agentic SDLC platform, with the same governance and access already in place.
What you can build
External interfaces allow you to design targeted, custom applications tailored to specific operational needs. Here are a few examples:
- Incident command hub: a mobile-friendly dashboard for on-call responders and executives, showing only the affected services and their remediation triggers. Clicking one triggers a Port workflow directly, multi-step incident response, approvals, even an AI agent's diagnosis, scoped to what that person is allowed to do in Port.
- Self-service kiosk: a kiosk for requesting sandboxes, databases, or software licenses through a form built exactly to your team's process. Submitting it triggers a Port workflow, provisioning, approvals, catalog updates, under the requester's own identity.
How access works
An OAuth app belongs to the Port organization it was created in and can't read or act in any other organization, even one the same admin manages.
A person signing into your app goes through Port's own authentication, including SSO if your organization has it configured, and approves access on Port's consent screen. Port then issues your app a token scoped to that person alone. The token can do only what that person could already do by signing into Port directly: the same roles, the same permissions, the same data scoping. You don't reimplement access control or governance in your app, Port enforces it on every request.
An OAuth app is not the same as the client ID and secret under Credentials, which create an access token tied to your whole organization. An OAuth app instead signs in as a specific person: every action it takes carries that person's own role and permissions, not a shared service identity.
Getting started
1. Connect Port MCP
Connect the Port MCP server to your IDE or vibe coding tool (Lovable, v0, Bolt, or similar).
2. Add sign-in with Port, in one prompt
Ask it:
Create an app that authenticates with Port. Use the `port-create-custom-oauth-app` skill.
Behind the scenes, this registers an OAuth app for you and wires up sign-in, so anyone who logs in through it gets access to your organization's data, in their own role. Prefer to set up the OAuth app yourself first? See Manage your OAuth apps.
3. Build on top of Port
Once sign-in works, specify what you want the app to do: read from the context lake, trigger workflows, or use any existing Port capability. For example:
Build a chat assistant that helps my team mitigate incidents in real time, using Port's context lake to pull in affected services, and can trigger our coding agent with the right context when I ask it to.
From here, your app can read Port's context lake and call workflows exactly as that signed-in user could from Port itself.
Implement sign-in manually
Not following steps 1-3 with an AI agent? Once you have a client ID and redirect URI (see Manage your OAuth apps), the flow is the standard OAuth 2.0 authorization code flow:
- When someone clicks Sign in with Port in your app, redirect them to Port's authorization URL with your
client_id,redirect_uri, and the scopes you need. - Port shows its own sign-in screen (including SSO, if your organization has it configured), then asks the person to approve access to your app.
- Port redirects back to your
redirect_uriwith an authorization code. - Your app exchanges that code, along with your
client_id, for an access token at Port's token endpoint. - Call the Port API with that access token. Every request runs as that signed-in person, scoped to their own role and permissions.
Host on GitHub Pages or any static host
An external interface doesn't need a backend. You can host a fully static single-page app (SPA) on GitHub Pages, or any other static host, and have it sign users in to Port and call the Port API as them, straight from the browser.
-
Register the app. An organization admin registers an OAuth app in Port. The quickest way is to ask the Port MCP server:
Register an External Interface app with redirect URI https://<org>.github.io/<repo>/callback.htmlYou get back a client ID. These are public PKCE clients, so there's no client secret to protect.
-
Sign in. The SPA runs a standard Authorization Code flow with PKCE (
S256) against Port's Auth0, withaudienceset to your region's API base URL (https://api.port.iofor EU,https://api.us.port.iofor US). The code-for-token exchange happens directly from the browser. -
Call the API. Send the access token as a
Bearertoken. Calls run with the signed-in user's own Port permissions. Port allows CORS requests from the origins of the app's registered redirect URIs, sofetchworks from the browser without a proxy.
Keep the following in mind:
- Redirect URIs must match exactly. Wildcards aren't supported, each app can have at most 5 redirect URIs, and
http://is allowed only forlocalhostand127.0.0.1. See Redirect URI rules for the full list. - Storing tokens in the browser comes with the usual XSS trade-offs, so keep your site's dependencies to a minimum.
For a working reference, see the external-interfaces-gh-pages-poc repository: plain JavaScript with no build step, about 300 lines. Its README includes the MCP prompt and a step-by-step guide for testing it in your own organization.
Troubleshooting
The redirect_uri sent to /authorize doesn't byte-for-byte match one of the URIs registered on the OAuth app. Common causes:
- Scheme, host, port, or trailing slash differs (
httpvshttps,:3000vs:5173, a trailing/present or absent). - The app is running on a different origin than what's registered. This is especially common on hosted app builders (Lovable, v0, Bolt), which often expose multiple origins for the same app, a live-preview URL, a stable project URL, a published URL, and only some of those get registered.
- The registered URI is correct, but it's a live-preview origin gated behind the platform's own login wall. Port's redirect technically lands there, but the page needs a separate login just to view it, so it looks identical to a mismatch from the outside.
To fix it:
- Open browser devtools on the running app and read
window.location.origin(or check the address bar). Don't infer the origin from the platform's name or docs. Confirm it matches, character for character, one of the app's registered redirect URIs. - If it doesn't match, register it. Updating an OAuth app's redirect URIs replaces the whole set, so include every URI still in use (localhost, preview, published), not just the new one.
- If the URI is registered but still fails, confirm the app is actually published and public. A redirect URI that requires the platform's own login to view can never receive Port's redirect, no matter how correctly it's registered.
Lovable, v0, Bolt, and similar platforms render the app's live preview inside an <iframe>. Port's sign-in page refuses to be framed, so if sign-in navigates the app's current window or frame directly to Port (a plain top-level redirect), the browser blocks it outright, sometimes silently, sometimes with an error like <domain> refused to connect. This isn't a wrong-URL or CORS problem, even though it can look like one.
To fix it, don't redirect the app's own window or frame to Port, open a new window for the auth flow instead:
- On click, open a blank window synchronously, before any
await, so popup blockers don't intervene, then navigate that window to/authorize. - Carry the PKCE
code_verifierthrough thestateparam rather thansessionStorageorlocalStorage. The popup starts cross-origin and doesn't reliably share storage with the window that opened it. - The popup's
/callbackroute finishes the token exchange itself, then sends the tokens back towindow.openerwithpostMessageand closes itself. The original, iframed window picks this up with a message listener and switches to authenticated. - Only fall back to a top-level redirect if the popup itself gets blocked. Inside an iframe, that breaks out of the whole embedding page, so treat it as a last resort, not the default path.