tnl.dev :: docs
serverless http.
Use a native connection owner from a provider-authenticated Node function.
fetch with an application credential
Call an HTTP API from your server using fetch, without starting a native tnl
process. First, give your app permission to visit the service:
tnl auth register inventory-reader \
--visit svc_4f90D13A42bKs7pQ8tM2Uv \
--output-file /private/tnl/inventory.jsonReplace the service ID with yours and use an existing private directory. Store
the JSON file as a server-side secret, then pass its contents to createTnl.
For example, read inventory from /stock:
import { createTnl, type TnlApplicationCredential } from "@tnldotdev/tnl/web";
export async function readStock(credential: TnlApplicationCredential) {
const app = createTnl({ credential });
try {
const api = await app.connect("svc_4f90D13A42bKs7pQ8tM2Uv");
return await (await api.fetch("/stock")).json();
} finally {
await app.close();
}
}You can also pass the complete JSON string from your secret store. The SDK does
not read TNL_CREDENTIAL automatically. Keep the document out of browser bundles
and public environment variables.
Use a saved public URL ID or service ID, rather than a project name such as
inventory. Control supplies its address and checks your app's visit grant.
Other grants cannot substitute for denied access, and the SDK never falls back
to your personal login. Register credentials during setup, not in request handlers.
This path works with public URLs and publicly reachable HTTP services. For a connector-only service or database, use the native runtime. Both need matching prerelease builds.
To reuse an HTTP client across requests, give the instance a client factory:
const getInventory = app.client("svc_4f90D13A42bKs7pQ8tM2Uv", {
create: ({ fetch }) => ({ stock: async () => (await fetch("/stock")).json() }),
close: async () => {},
});
const inventory = await getInventory();await getInventory() checks access and refreshes permission when needed.
Routine renewal reuses the client. An endpoint or trusted-service change gets a
new client; an old Fetch handle refuses further requests with changed settings.
Requests and uploads are never replayed after a failure.
Read the response body before closing a request-local instance. Close a shared
instance at application shutdown. app.close() waits for pending setup, closes
clients, then cancels remaining response streams. Retry the close if cleanup
fails. Canceling one request does not cancel another request's shared permission
check.
The /web entry point needs Web Crypto and Fetch, but no Node APIs or native
binary. Use the native entry point to publish an app.
fetch with an explicit visitor token
For a simple script or function, you can use a visitor token instead of an application document. Create one for your public URL or HTTP service:
tnl share token create preview.example.com --expires-in 7dFor a service, use tnl share service token create SERVICE --expires-in 7d.
Store the printed token in a server-side secret:
import { createTnl } from "@tnldotdev/tnl/web";
export async function handle(env: { TNL_CREDENTIAL: string }) {
const app = createTnl({ credential: env.TNL_CREDENTIAL });
try {
const api = app.connect("https://preview.example.com");
return await (await api.fetch("/stock")).json();
} finally {
await app.close();
}
}Token connections use a synchronous connect(origin). Application documents use
await connect(resourceID). Both return { url, fetch } settings.
Call fetch as usual: it adds Tnl-Visitor-Token and preserves your app's headers
and body. It leaves out browser credentials, returns redirects without following
them, and does not retry requests. Cross-origin requests and caller-supplied tnl
authentication headers are rejected. Pass your API's own credentials through its
usual headers.
For a cached client, use app.client(origin, { create, close }) with the same
cleanup rules as the application-document example. The native SDK entry point
also accepts these explicit credentials.
call with a python application credential
Python can use the same application document. For example, with HTTPX:
import os
import httpx
import tnl
async def read_stock():
async with tnl.create_tnl(credential=os.environ["TNL_CREDENTIAL"]) as app:
get_api = app.client(
"svc_4f90D13A42bKs7pQ8tM2Uv",
create=lambda connection: httpx.AsyncClient(
base_url=connection.url, transport=connection.transport,
),
close=httpx.AsyncClient.aclose,
)
api = await get_api()
return (await api.get("/stock")).json()Use the registration command above during setup. Pass the complete JSON string or parsed dictionary explicitly. Python signs the permission request with the document's key and uses only its selected visit grant. This HTTP path needs no native binary or personal login.
await app.connect(resource_id) returns HTTPClientSettings if you want to
construct the client yourself. Application-document connections need the await;
reading settings before preparation fails. Control supplies the hostname and port
for a public URL or publicly reachable HTTP service.
Ordinary renewal reuses the client; changed endpoints require a new one. Concurrent
callers share permission checks, and canceling one does not cancel the others.
Leaving the async context waits for cleanup. If it fails, retry app.aclose() on
that instance. Requests and uploads are never replayed.
call with a python visitor token
For a visitor token, use the HTTPS origin instead of a resource ID:
import os
import httpx
import tnl
async def read_stock():
async with tnl.create_tnl(credential=os.environ["TNL_CREDENTIAL"]) as app:
get_api = app.client(
"https://preview.example.com",
create=lambda connection: httpx.AsyncClient(
base_url=connection.url, transport=connection.transport,
),
close=httpx.AsyncClient.aclose,
)
api = await get_api()
return (await api.get("/stock")).json()Use an origin without a trailing slash and keep any assigned service port, such
as https://api.example.com:20001. app.connect(origin) returns the same settings
synchronously if you want to construct the client yourself. Token HTTP does not
take native directory, server, or client-state options.
The transport adds the tnl token and preserves your app's headers and bodies. It rejects cross-origin requests and caller-supplied tnl authentication headers, and leaves out browser access cookies. Redirects are returned without following them, even if your HTTP client enables redirects. Requests are not retried; TLS verification stays on, ambient proxies are ignored, and diagnostics omit the token.
Read responses before leaving the async context. It closes clients, waits for
remaining request and response cleanup, then closes the shared transport.
Closing one client alone leaves the others running. Keep each instance on one
event loop and create a new one after a fork. Close shared instances at shutdown
and retry aclose() if cleanup fails.
call a service from a node function
For a private API, run a publisher in its network and approve your function with a workload policy. Then connect inside the handler:
import { tnl } from "@tnldotdev/tnl";
export async function GET() {
const inventory = await tnl.connect("inventory");
return inventory.fetch("/stock");
}The await starts or reuses a local connector and checks access before returning
the HTTP client. Keep it even when a previous invocation already connected.
This path needs a native process and matching prerelease packages;
deployed function startup and freeze/thaw still need live checks.
approve the deployment
Declare your service and provider under workloads, then generate the manifest and apply the configuration. Run the assigned publisher where it can reach the API.
For Vercel, apply the real team, project, and environment. If staging and production
use separate Vercel projects, each project's deployed website can have environment
production. A preview is a different environment. Use separate tnl configurations
for those projects rather than inferring identity from their deployment names.
At runtime, tnl reads .tnl/generated/workloads.json from the process working
directory. It selects one matching provider workload; TNL_WORKLOAD chooses one
explicitly. It does not evaluate TypeScript, inspect Git, or trust local Vercel
project metadata.
The SDK reads Vercel's token from the current request when you prepare a connection, not at module initialization. Configured token-file/environment sources also use the app's current inputs. Missing tokens or rejected identities fail before the API call; they never trigger a CLI login or use your developer session.
own a connection scope
Use createTnl to give an app its own project selection, workload token callback,
and cleanup. For example, keep a PostgreSQL pool across requests:
import { createTnl } from "@tnldotdev/tnl";
import { Pool } from "pg";
const app = createTnl({
directory: "/srv/checkout",
credential: { workload: "worker", token: async () => acquireCurrentAssertion() },
});
const getDb = app.client("orders", {
create: (connection) => new Pool({ ...connection, user: "checkout" }),
close: (pool) => pool.end(),
});
const db = await getDb();
await db.query("select 1");
// during application shutdown
await app.close();acquireCurrentAssertion is your provider integration. It reads the current
request's token, rather than a token captured at startup. Supply database
credentials through the driver's usual options.
Each getter checks access and reuses the pool for the same identity and connection settings. Renewal alone keeps the pool. Changed settings get a new pool; old ones remain until cleanup without gaining extra permission. Select a client before a transaction and keep it for the whole transaction.
At shutdown, app.close() stops accepting work, waits for setup, closes its
clients, and stops its native processes. Other instances keep running. Retry the
close if cleanup fails. A shared instance should stay open between requests.
package the native runtime
Include these in your server build:
- Matching
@tnldotdev/tnland platform-native packages, including the executable, CommonJS connection bridge, runtime dependencies, and legal files. .tnl/generated/workloads.json, generated from the configuration you applied, at the runtime working directory.- A Node runtime that permits subprocesses, private writable temporary state, and loopback sockets.
Externalize the SDK when bundling so its package-relative bridge keeps working. Build for the function's OS and architecture: a macOS binary cannot run in a Linux function. Keep cached sessions, private keys, owner handles, and build-time platform tokens out of the artifact. The connector acquires its credentials when the function runs.
The publisher's upstream can stay private. Connector access works with private and public services and preserves their hostname and allocated port. Your app needs no tnl client certificate.
preserve application authentication and work
Your API can still require its own login. Pass its token through the usual header:
const inventory = await tnl.connect("inventory");
return inventory.fetch("/stock", {
headers: { Authorization: `Bearer ${applicationToken}` },
});Your app supplies applicationToken. It is separate from the platform token tnl
uses to check whether this deployment can reach the service.
tnl renews permission while the session is valid. Existing streams keep their original identity and deadline; a new session cannot extend them. If a stream fails, reconnect for later work. Do not automatically replay an upload, SQL statement, or transaction that may already have reached the target.
Use a client factory to select current pool settings. If you manage pools yourself, drain and replace a pool when its listener or TLS settings change. Rebuild any adapter, such as BetterAuth's, that retained the old pool. Keep cancellation, streaming backpressure, and origin checks in your application.
qualify the function lifecycle
Before relying on a deployed function, test it after a cold start, during concurrent requests, and after the platform suspends and resumes it. Also test token expiry, revocation, connector replacement, and streaming cancellation. For databases, include your real pool and auth adapter. See release status for what has been checked locally and what still needs a live deployment.