Build a DEeplinq SaaS app
Build and host an independent app, deploy it on Railway, and register it in the SaaS catalog.
A DEeplinq SaaS app is an independently hosted website. Partners own their source and deployment pipeline. The SaaS platform registers the app's metadata and URLs; it does not import the partner's source code.
This guide covers apps installed in the DEeplinq SaaS catalog. The Engine's applications manage product identity and issuers at a different layer.
Choose how the app opens
| Mode | App location | Global shell |
|---|---|---|
| External tab | The partner website in its own tab | The partner owns the full page, user menu and return link |
| Embedded | The partner website in an iframe | DEeplinq renders the shell around the app content |
An app can support both modes. Keep external launch available as a fallback. The JavaScript SDK and shell are framework independent; React helpers are optional. Partners can use their own UI kit. External apps do not mount the DEeplinq shell; embedded apps render content inside the host shell.
Connected users sign in through DEeplinq's Better Auth session. In external mode, the partner server completes delegated authorization and maintains an app-local session. Cookies are not shared between websites. In embedded mode, the verified host channel performs authorized operations without exposing the DEeplinq cookie to the iframe.
Integrated operations use the TanStack-hosted Platform API, including SaaS policy and its existing Engine connection. The app does not need a separate Engine connection or a machine-access grant.
Load external pages on the server
Use TanStack route loaders and server functions, as in the SaaS application. The sample resolves the incoming app session and loads the user's profile, active workspace, permitted models and any URL-selected saved result before rendering HTML. An absent session starts the Better Auth handoff on the server. Failed or declined attempts show a retry form instead of automatically starting another attempt.
loadExternalAppPage from @deeplinq/app-sdk/server performs these reads through
the existing adapter in-process. Forward its Set-Cookie and no-store response
headers. Keep adapter instances, headers and credentials server-side; serialize only
the safe context, models, saved result and anti-CSRF control. Do not cache user data
in a shared process cache.
The browser's createExternalAppTransport({ mountPath, initialState: { context, csrf } })
consumes the server context once. Seed the model/result UI from the same loader data,
so mounting does not repeat session or model discovery. Later refreshes and operations
still verify live authority. Generation and streaming remain user-triggered.
Embedded mode is different: the partner server cannot read the host's session. The app waits for the verified host handshake in the browser; it must not reuse a partner cookie as iframe authority.
Render your own user navigation
An external app uses the entire page. Provide a user menu in your own UI kit and
an explicit Back to DEeplinq action. Do not mount AppShellElement in this mode.
Read the verified runtime.context after connection and subscribe to runtime
updates. It includes:
| Context | Profile information |
|---|---|
user | id, name, email, image |
organization | DEeplinq id, name, kind (personal or team), membership role |
theme | The user's active appearance preference |
Email, image and role are optional for compatibility with older hosts. Use initials when no image is supplied; large or unsupported profile images are omitted. These are display details, not permission checks. Never request session cookies or secrets to render a menu. Clear profile information when the runtime loses authority.
The sample uses the framework-independent createExternalShellNavigation helper
for trusted account, appearance, workspace and sign-out destinations. This helper
does not render a shell. Its home action returns to DEeplinq's /apps page without
signing the user out. See the sample's src/browser.ts and src/view.tsx for wiring
these actions to your own controls. In embedded mode, let the host provide global
navigation instead of duplicating it inside the iframe.
Start with the sample
The single sample is apps/saas-sample-app in deeplinq-frontends. It uses
TanStack Start for server rendering, React and Material UI for its content, and
the public SDK for integration. It deploys as its own Node service.
For development outside that repository, obtain a matching release containing
app-sdk.tgz, app-shell.tgz and app-sdk-react.tgz. These instructions use local
release archives; they do not assume that the packages are published on npm.
Run from a directory where app-tools and saas-sample-app do not exist:
mkdir app-tools
cd app-tools
npm init -y
npm install --ignore-scripts --no-audit --no-fund /absolute/release/app-sdk.tgz
./node_modules/.bin/create-deeplinq-app ../saas-sample-app --archives /absolute/release
cd ../saas-sample-app
npm install --ignore-scripts --no-audit --no-fund
cp server/.env.example server/.env
npm run typecheck
npm run buildUse Node 22.12 or newer. The generator supplies editable source and local package archives. It does not install the app into DEeplinq, register it, create credentials or deploy it. Configure runtime variables according to the generated README.
Deploy on Railway
Configure the values directly in the Railway console. No railway.json or
RAILWAY_DOCKERFILE_PATH environment variable is required.
| Setting | Generated standalone project | Sample in deeplinq-frontends |
|---|---|---|
| Root directory | Standalone repository root | Monorepo root |
| Builder | Railpack | Dockerfile |
| Dockerfile path | Not needed | /apps/saas-sample-app/Dockerfile |
| Build command | npm run build | Use the Dockerfile build |
| Pre-deploy command | npm run db:migrate | node .output/migrate.mjs |
| Start command | npm start | node scripts/start.mjs |
| Health check | /health, timeout 60 seconds | /health, timeout 60 seconds |
| Restart policy | On failure | On failure |
Provision a dedicated PostgreSQL database and set DATABASE_URL to its
connection string. Keep client credentials and encryption keys in private service
variables. The sample must not use the SaaS database or its bun run bootstrap
pre-deploy command.
The monorepo build needs the repository root so its Dockerfile can build the shared packages. Set these watch paths on that service:
/apps/saas-sample-app/**
/packages/app-sdk/**
/packages/app-shell/**
/packages/app-sdk-react/**
/package.json
/bun.lock
/patches/**Avoid a shared repository-root Railway config file: its settings can select the SaaS image and bootstrap command for the sample service. The sample Dockerfile builds standalone Node output and bundles the migration runner. Migrations run before deployment and are safe to repeat.
A successful /health response confirms the HTTP server is running. Registration,
user authorization and connected Engine operations require the setup below.
Register manually, then install for an organization
- Configure
DATABASE_URL,PARTNER_PUBLIC_ORIGIN, the actualDEEPLINQ_ISSUER, a stableDEEPLINQ_APP_ID, and requestedDEEPLINQ_CAPABILITIES. Public metadata and signed installation work before user-login credentials exist. No metadata-only flag is needed; legacyPARTNER_METADATA_ONLYvalues are ignored. - An authorized platform administrator opens Platform Apps, enters the app's
public URL and
/.well-known/deeplinq-app.json, and chooses its audience: All customers, Teams only, Individuals only, or Specific team. Registration adds the app to the catalog. It does not install it for an organization. - Store the returned
DEEPLINQ_CLIENT_IDand one-timeDEEPLINQ_CLIENT_SECRETprivately on the partner server. Configure the registered authorization, token, resource, API and callback endpoints andPARTNER_ENCRYPTION_KEYS, then restart. These settings enable user login; they do not gate signed installation. - An eligible organization's authorized administrator installs the app from
Organization → Apps. DEeplinq marks the installation pending, calls the partner's signed
/installendpoint, and activates it only after the partner acknowledges durable preparation.
When opened externally, the sample automatically starts the Better Auth handoff
if it needs a user session. An existing DEeplinq sign-in is reused; any required
authorization remains enforced. Failed or declined callbacks show an explicit
retry instead of redirecting repeatedly. Embedded mode uses the host connection.
Opening the app never starts model work. Serve app documents with
Referrer-Policy: strict-origin: native authorization form submissions must
preserve the Origin header for CSRF validation. no-referrer would turn it into
null and cause the SDK to reject the request.
Always reuse the canonical DEeplinq organization ID supplied to /install,
including for personal organizations. Preserve the personal/team kind. Do not
introduce a second tenant ID that DEeplinq would have to map. Retry uncertain
installation delivery with the same operation and payload.
Use exact registered HTTPS origins and callbacks in production. External mode
uses /external.html; embedded mode uses /embedded.html and requires compatible
framing policy. Installing an app provisions organization state; user actions
explicitly start model work.
Partner connection variables
For the hosted platform at https://platform.ondeeplinq.com, use these exact
server-side values. For another deployment, replace that origin and keep the paths.
The partner app's own origin belongs in PARTNER_PUBLIC_ORIGIN, not these values.
| Variable | Value |
|---|---|
DEEPLINQ_ISSUER | https://platform.ondeeplinq.com/api/auth |
DEEPLINQ_AUTHORIZATION_ENDPOINT | https://platform.ondeeplinq.com/api/auth/oauth2/authorize |
DEEPLINQ_TOKEN_ENDPOINT | https://platform.ondeeplinq.com/api/auth/oauth2/token |
DEEPLINQ_RESOURCE | https://platform.ondeeplinq.com/api/apps |
DEEPLINQ_API_BASE_URL | https://platform.ondeeplinq.com/api/apps/v1 |
The issuer publishes discovery at
https://platform.ondeeplinq.com/.well-known/oauth-authorization-server/api/auth.
Use the authorization and token endpoints from that document. The resource
identifies the delegated API audience; the API base includes /v1 for requests.
The callback defaults to <PARTNER_PUBLIC_ORIGIN>/_deeplinq/callback; it must
match the registered callback exactly, including a custom API mount if used.
Keep DEEPLINQ_CLIENT_SECRET, PARTNER_ENCRYPTION_KEYS, and DATABASE_URL
private on the partner server. Preserve encryption keys across deployments so
existing app sessions remain readable. No DEeplinq session cookie is copied to
the partner host.
An existing valid app session opens directly. DEeplinq remembers approved access for the same user, workspace and app configuration. New permissions or changed installation authority can require approval again. This is separate from the administrator installing the app. Switching workspace or losing membership invalidates the old authority; reconnect from the intended workspace. A session with no active workspace is refused, never assigned another organization's data.
Signed authorization URLs
When implementing login, consent, or resume pages, preserve the original query
from the request or browser history. Better Auth repeats signed parameter names
in ba_param; TanStack's JSON search serialization converts them into an array
and invalidates the request. Do not reconstruct signed queries from
location.searchStr or remove signature validation. The SaaS auth screens read
raw history, including Start's request-local history during server rendering.