Guides

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

ModeApp locationGlobal shell
External tabThe partner website in its own tabThe partner owns the full page, user menu and return link
EmbeddedThe partner website in an iframeDEeplinq 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:

ContextProfile information
userid, name, email, image
organizationDEeplinq id, name, kind (personal or team), membership role
themeThe 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 build

Use 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.

SettingGenerated standalone projectSample in deeplinq-frontends
Root directoryStandalone repository rootMonorepo root
BuilderRailpackDockerfile
Dockerfile pathNot needed/apps/saas-sample-app/Dockerfile
Build commandnpm run buildUse the Dockerfile build
Pre-deploy commandnpm run db:migratenode .output/migrate.mjs
Start commandnpm startnode scripts/start.mjs
Health check/health, timeout 60 seconds/health, timeout 60 seconds
Restart policyOn failureOn 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

  1. Configure DATABASE_URL, PARTNER_PUBLIC_ORIGIN, the actual DEEPLINQ_ISSUER, a stable DEEPLINQ_APP_ID, and requested DEEPLINQ_CAPABILITIES. Public metadata and signed installation work before user-login credentials exist. No metadata-only flag is needed; legacy PARTNER_METADATA_ONLY values are ignored.
  2. 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.
  3. Store the returned DEEPLINQ_CLIENT_ID and one-time DEEPLINQ_CLIENT_SECRET privately on the partner server. Configure the registered authorization, token, resource, API and callback endpoints and PARTNER_ENCRYPTION_KEYS, then restart. These settings enable user login; they do not gate signed installation.
  4. An eligible organization's authorized administrator installs the app from Organization → Apps. DEeplinq marks the installation pending, calls the partner's signed /install endpoint, 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.

VariableValue
DEEPLINQ_ISSUERhttps://platform.ondeeplinq.com/api/auth
DEEPLINQ_AUTHORIZATION_ENDPOINThttps://platform.ondeeplinq.com/api/auth/oauth2/authorize
DEEPLINQ_TOKEN_ENDPOINThttps://platform.ondeeplinq.com/api/auth/oauth2/token
DEEPLINQ_RESOURCEhttps://platform.ondeeplinq.com/api/apps
DEEPLINQ_API_BASE_URLhttps://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.

On this page