Backend Setup: JavaScript/TypeScript
This guide walks you through instrumenting a Node.js application (JavaScript or TypeScript) with the SF Veritas SDK.
Installation
Install the SF Veritas package:
npm install @sailfish-ai/sf-veritas
Or with yarn:
yarn add @sailfish-ai/sf-veritas
Basic Setup
Add the following to your application's entry point (e.g., index.ts, app.ts, or server.ts). The key is to wrap the initialization in a NODE_ENV check so it only runs during local development:
// Initialize SF Veritas ONLY in development mode
if (process.env.NODE_ENV === 'development') {
import('@sailfish-ai/sf-veritas').then(({ setupInterceptors }) => {
setupInterceptors({
apiKey: 'sf-veritas-local',
apiGraphqlEndpoint: 'http://localhost:6776/graphql/',
serviceIdentifier: 'my-backend-service',
serviceVersion: '1.0.0',
// Your company's allowed domains — add more here if you have additional internal hosts.
domainsToPropagateHeadersTo: ["*"],
});
});
}
// Your application code continues below...
import express from 'express';
const app = express();
// ... rest of your application
Configuration Options
| Option | Type | Required | Description |
|---|---|---|---|
apiKey | string | Yes | Use "sf-veritas-local" for local development |
apiGraphqlEndpoint | string | Yes | URL of the local collector (default port 6776) |
serviceIdentifier | string | Yes | Unique name for your service |
serviceVersion | string | No | Version of your service |
gitSha | string | No | Git commit SHA; auto-detected from common CI env vars |
debug | boolean | No | Enable verbose debug logging |
Header-Propagation Options
Control which outbound domains receive Sailfish's distributed-tracing header (X-Sf3-Rid). Mostly relevant once you take the SDK beyond the local collector — if you're just running against localhost:6776 in dev you can ignore these.
| Option | Type | Default | Description |
|---|---|---|---|
domainsToPropagateHeadersTo | string[] | ["*"] | Allowlist of domains that SHOULD receive the tracing header. Wildcard syntax: "*", "*.example.com", "api.example.com:8080", "api.example.com/v1/*". Default ["*"] = allow all. Set to [] to disable propagation entirely. |
domainsToNotPropagateHeadersTo | string[] | [] | Denylist of domains to exclude. Takes precedence over the allowlist. |
retryOnClientError | 'all' | 'idempotent' | 'none' | 'all' | How to handle 400/403 responses on requests with our tracing header. 'idempotent' is safest for payment/booking backends. Also settable via SF_RETRY_ON_CLIENT_ERROR. |
propagate = matches(url, domainsToPropagateHeadersTo)
&& NOT matches(url, domainsToNotPropagateHeadersTo)
&& NOT matches(url, BUILT_IN_DEFAULTS)
Built-in defaults are the well-known analytics / auth / CDN domains (Twitter, Gravatar, Google APIs, AWS, Zendesk, Smooch) that should never receive Sailfish tracing headers regardless of customer config.
Configuration File
For more control over what gets captured, create a .sailfish file in your project root:
{
"capture": {
"console": true,
"exceptions": true,
"functions": true,
"network": true
},
"sampling": {
"rate": 1.0,
"maxEntriesPerSecond": 1000
},
"filters": {
"excludePaths": [
"node_modules",
"dist"
],
"excludeFunctions": [
"internalHelper"
]
}
}
Configuration Options
capture
| Option | Default | Description |
|---|---|---|
console | true | Capture console.log/info/warn/error |
exceptions | true | Capture unhandled exceptions |
functions | true | Capture function execution traces |
network | true | Capture outgoing HTTP requests |
sampling
| Option | Default | Description |
|---|---|---|
rate | 1.0 | Sampling rate (0.0 to 1.0, where 1.0 = 100%) |
maxEntriesPerSecond | 1000 | Rate limit for telemetry entries |
filters
| Option | Default | Description |
|---|---|---|
excludePaths | [] | File paths to exclude from tracing |
excludeFunctions | [] | Function names to exclude from tracing |
Framework Examples
Express.js
// server.ts
import express from 'express';
// Initialize SF Veritas only in development
if (process.env.NODE_ENV === 'development') {
import('@sailfish-ai/sf-veritas').then(({ setupInterceptors }) => {
setupInterceptors({
apiKey: 'sf-veritas-local',
apiGraphqlEndpoint: 'http://localhost:6776/graphql/',
serviceIdentifier: 'express-api',
serviceVersion: '1.0.0',
});
});
}
const app = express();
app.get('/api/users', (req, res) => {
console.log('Fetching users'); // This will appear in SF Veritas Console
// ...
});
app.listen(3000, () => {
console.log('Server started on port 3000');
});
NestJS
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
// Initialize SF Veritas only in development
if (process.env.NODE_ENV === 'development') {
const { setupInterceptors } = await import('@sailfish-ai/sf-veritas');
setupInterceptors({
apiKey: 'sf-veritas-local',
apiGraphqlEndpoint: 'http://localhost:6776/graphql/',
serviceIdentifier: 'nestjs-api',
serviceVersion: '1.0.0',
});
}
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
Fastify
// app.ts
import Fastify from 'fastify';
// Initialize SF Veritas only in development
if (process.env.NODE_ENV === 'development') {
import('@sailfish-ai/sf-veritas').then(({ setupInterceptors }) => {
setupInterceptors({
apiKey: 'sf-veritas-local',
apiGraphqlEndpoint: 'http://localhost:6776/graphql/',
serviceIdentifier: 'fastify-api',
serviceVersion: '1.0.0',
});
});
}
const fastify = Fastify({ logger: true });
fastify.get('/', async (request, reply) => {
console.log('Handling request');
return { hello: 'world' };
});
fastify.listen({ port: 3000 });
Koa
// app.ts
import Koa from 'koa';
import Router from '@koa/router';
if (process.env.NODE_ENV === 'development') {
import('@sailfish-ai/sf-veritas').then(({ setupInterceptors }) => {
setupInterceptors({
apiKey: 'sf-veritas-local',
apiGraphqlEndpoint: 'http://localhost:6776/graphql/',
serviceIdentifier: 'koa-api',
serviceVersion: '1.0.0',
});
});
}
const app = new Koa();
const router = new Router();
router.get('/', (ctx) => {
ctx.body = { hello: 'world' };
});
app.use(router.routes());
app.listen(3000);
Hapi
// server.ts
import Hapi from '@hapi/hapi';
if (process.env.NODE_ENV === 'development') {
import('@sailfish-ai/sf-veritas').then(({ setupInterceptors }) => {
setupInterceptors({
apiKey: 'sf-veritas-local',
apiGraphqlEndpoint: 'http://localhost:6776/graphql/',
serviceIdentifier: 'hapi-api',
serviceVersion: '1.0.0',
});
});
}
const init = async () => {
const server = Hapi.server({ port: 3000, host: 'localhost' });
server.route({
method: 'GET',
path: '/',
handler: () => ({ hello: 'world' }),
});
await server.start();
console.log('Server running on %s', server.info.uri);
};
init();
Hono
// app.ts
import { Hono } from 'hono';
import { serve } from '@hono/node-server';
if (process.env.NODE_ENV === 'development') {
import('@sailfish-ai/sf-veritas').then(({ setupInterceptors }) => {
setupInterceptors({
apiKey: 'sf-veritas-local',
apiGraphqlEndpoint: 'http://localhost:6776/graphql/',
serviceIdentifier: 'hono-api',
serviceVersion: '1.0.0',
});
});
}
const app = new Hono();
app.get('/', (c) => c.json({ hello: 'world' }));
serve({ fetch: app.fetch, port: 3000 });
Apollo Server (GraphQL)
// index.ts
import { setupInterceptors } from '@sailfish-ai/sf-veritas';
if (process.env.NODE_ENV === 'development') {
setupInterceptors({
apiKey: 'sf-veritas-local',
apiGraphqlEndpoint: 'http://localhost:6776/graphql/',
serviceIdentifier: 'graphql-api',
serviceVersion: '1.0.0',
});
}
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
const server = new ApolloServer({
typeDefs: `type Query { hello: String }`,
resolvers: { Query: { hello: () => 'Hello, world!' } },
});
startStandaloneServer(server, { listen: { port: 4000 } });
Mercurius (Fastify GraphQL)
// app.ts
import fastify from 'fastify';
import mercurius from 'mercurius';
if (process.env.NODE_ENV === 'development') {
import('@sailfish-ai/sf-veritas').then(({ setupInterceptors }) => {
setupInterceptors({
apiKey: 'sf-veritas-local',
apiGraphqlEndpoint: 'http://localhost:6776/graphql/',
serviceIdentifier: 'graphql-api',
serviceVersion: '1.0.0',
});
});
}
const app = fastify();
const schema = `
type Query {
hello: String
}
`;
const resolvers = {
Query: {
hello: () => 'Hello from Mercurius!',
},
};
app.register(mercurius, { schema, resolvers, graphiql: true });
app.listen({ port: 3000 });
Nuxt.js
Create a server plugin to initialize Sailfish in development:
// server/plugins/sailfish.ts
export default defineNitroPlugin(async () => {
if (process.env.NODE_ENV === 'development') {
const { setupInterceptors } = await import('@sailfish-ai/sf-veritas');
setupInterceptors({
apiKey: 'sf-veritas-local',
apiGraphqlEndpoint: 'http://localhost:6776/graphql/',
serviceIdentifier: 'nuxt-app',
serviceVersion: '1.0.0',
});
}
});
MeteorJS
// server/instrumentation.js
export async function register() {
if (process.env.NODE_ENV === 'development') {
const { setupInterceptors } = await import('@sailfish-ai/sf-veritas');
setupInterceptors({
apiKey: 'sf-veritas-local',
apiGraphqlEndpoint: 'http://localhost:6776/graphql/',
serviceIdentifier: 'meteor-app',
serviceVersion: '1.0.0',
});
}
}
// server/main.js
import { register } from './instrumentation';
register().catch((err) => {
console.error('[Instrumentation] Failed to initialize:', err);
});
// ... your Meteor server code
Install via meteor npm install @sailfish-ai/sf-veritas. Meteor uses its own package manager, but npm packages work through meteor npm.
Next.js
Next.js has a built-in Instrumentation hook that runs before the application starts. Create an instrumentation.ts file in your project root (next to next.config.ts):
// instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
if (process.env.NODE_ENV === 'development') {
const { setupInterceptors } = await import('@sailfish-ai/sf-veritas');
setupInterceptors({
apiKey: 'sf-veritas-local',
apiGraphqlEndpoint: 'http://localhost:6776/graphql/',
serviceIdentifier: 'my-nextjs-app',
serviceVersion: '1.0.0',
});
}
}
}
Next.js calls the register() function once when a new Next.js server instance is started. The NEXT_RUNTIME check ensures the interceptors only run in the Node.js runtime (not Edge Runtime). This is the recommended way to initialize server-side telemetry in Next.js.
Function Span Profiling (Webpack Plugin)
To capture function execution traces (flamechart), add the FuncspanWebpackPlugin to your next.config.ts:
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
webpack: (config, { isServer, nextRuntime }) => {
if (isServer && nextRuntime === 'nodejs') {
const { FuncspanWebpackPlugin } = require(
'@sailfish-ai/sf-veritas/funcspan-webpack'
);
config.plugins.push(
new FuncspanWebpackPlugin({
enabled: true,
includeNodeModules: [], // Only instrument your application code
})
);
config.devtool = 'source-map';
}
return config;
},
};
export default nextConfig;
Environment Variables
Add a .env.local file for local development:
# .env.local
SAILFISH_GRAPHQL_ENDPOINT=http://localhost:6776/graphql/
Then reference it in your instrumentation.ts:
// instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
if (process.env.NODE_ENV === 'development') {
const { setupInterceptors } = await import('@sailfish-ai/sf-veritas');
setupInterceptors({
apiKey: 'sf-veritas-local',
apiGraphqlEndpoint:
process.env.SAILFISH_GRAPHQL_ENDPOINT ||
'http://localhost:6776/graphql/',
serviceIdentifier: 'my-nextjs-app',
serviceVersion: '1.0.0',
});
}
}
}
If your Next.js app has a frontend UI, you can also add the frontend recorder to capture browser-side telemetry (console logs, network requests, errors). The two work independently — instrumentation.ts captures server-side activity, while the frontend recorder captures client-side activity.
Async Queue Tracing
Sailfish automatically stitches producer → broker → consumer into a single trace when your code uses an instrumented async-queue library. Background jobs and Kafka events show up in the same trace as the HTTP request that enqueued them.
HTTP POST /enqueue ──► queue.add() / producer.send() ──► broker ──► worker.processJob / consumer.run
(inbound hop) (ENQUEUE hop) (CONSUME hop)
│─────────────── shared session + pageVisit ───────────────│
Supported libraries (zero-config)
| Library | Producer | Consumer |
|---|---|---|
| BullMQ | Queue.add, Queue.addBulk, FlowProducer.add | Worker.processJob |
| kafkajs | producer.send, producer.sendBatch, producer.transaction().send | consumer.run({ eachMessage / eachBatch }) |
| @confluentinc/kafka-javascript | both KafkaJS-compat and node-rdkafka modes | both modes |
Each works automatically — just import the library and the SDK's patches
wire up on setupInterceptors().
Example
if (process.env.NODE_ENV === 'development') {
const { setupInterceptors } = require('@sailfish-ai/sf-veritas');
setupInterceptors({
apiKey: 'sf-veritas-local',
serviceIdentifier: 'order-service',
apiGraphqlEndpoint: 'http://localhost:6776/graphql/',
});
}
import { Queue } from 'bullmq';
const queue = new Queue('orders', { connection: { host: 'redis' } });
// In your HTTP handler:
await queue.add('send-receipt', { orderId });
Open the Desktop App Console — you'll see one linked trace: the HTTP
request → an ENQUEUE hop for send-receipt → a CONSUME hop (from the
worker process) → any downstream HTTP calls the job makes.
Advanced BullMQ + Kafka scenarios
FlowProducer(BullMQ parent/child trees) — every node (parent and every descendant) gets its own rotated requestId while sharing session + pageVisit.- Delayed / repeating jobs (
{ delay, repeat: { every, limit } }or cron patterns) — your options are preserved; each dispatched instance gets a distinct requestId. - Kafka transactional producers (
producer.transaction()) — hops are emitted synchronously atsend()withmethod: "ENQUEUE_TX"so the UI can visually distinguish them.
Runtime controls
Set SF_DISABLE_QUEUE_PATCHES=true to skip every queue-library patch
without disabling the rest of the SDK.
On startup, Sailfish logs one info line listing which libraries were patched, skipped (not installed), and failed — useful for confirming the integration.
Caveats
- BullMQ sandboxed processors
(
new Worker(queueName, '/path/to/processor.js')) spawn a child Node process. Sailfish still emits the inbound CONSUME hop from the parent process, but outbound HTTP / nestedqueue.addcalls INSIDE the sandboxed file will only be traced if that file ALSO callssetupInterceptors(). - Kafka transactions emit hops on
send(), not oncommit(). An aborted transaction still leaves its hop in the timeline.
Verifying the Setup
- Start your application
- In VS Code, open the Command Palette (
Ctrl+Shift+P/Cmd+Shift+P) - Run
SF Veritas: Show Console Logs - Trigger some activity in your application
- You should see logs appearing in the Console panel
Performance
The SDK uses a Worker thread to send telemetry completely off the main event loop. All HTTP I/O (log collection, exception reporting, network hop tracking) happens in a background thread, resulting in near-zero overhead on your application's request handling.
| Environment | Telemetry Mode | Overhead |
|---|---|---|
| Standard Node.js (12+) | Worker thread (batched) | < 1% |
| Edge runtimes (see below) | Direct fetch (main thread) | ~5-20% depending on log volume |
Build Plugin: Zero-Cost Log Source Location
When you use a Sailfish build plugin (Webpack, Vite, Rollup, esbuild, or TSC), the SDK automatically injects source file and line number into every console.log/error/warn/info/debug call at build time — eliminating all runtime stack capture overhead (0µs per log call).
The build plugin appends a hidden sentinel to each console call that the SDK detects and strips before output. Your logs look exactly the same.
Setup per bundler:
Vite
// vite.config.ts
import { funcspanVitePlugin } from '@sailfish-ai/sf-veritas/plugins/funcspanVitePlugin';
export default defineConfig({
plugins: [
funcspanVitePlugin({ debug: false })
]
});
Webpack
// webpack.config.js
const { FuncspanWebpackPlugin } = require('@sailfish-ai/sf-veritas/plugins/funcspanWebpackPlugin');
module.exports = {
plugins: [
new FuncspanWebpackPlugin({ debug: false })
]
};
TypeScript (tsc)
{
"scripts": {
"build": "tsc && node -e \"require('@sailfish-ai/sf-veritas/plugins/funcspanTscPlugin').cli()\""
}
}
The build plugin also enables automatic function span instrumentation (Flamechart). If you're already using the Sailfish build plugin, console location injection is enabled automatically.
Environment Variables
| Variable | Default | Description |
|---|---|---|
SF_LOG_LOCATION | auto | Source location mode: auto (build-inject if available, else V8), v8 (runtime stack capture), off (disabled) |
SF_FLUSH_TELEMETRY_IN_BATCH | 0 | Set to 1 to send telemetry as batched JSON arrays (fewer HTTP connections). Set to 0 for individual POSTs (default) |
SF_BATCH_FLUSH_INTERVAL_MS | 50 | How often the Worker thread flushes queued telemetry (ms). Lower = less latency, higher = fewer HTTP calls |
SF_NBPOST_DISABLE_BATCHING | 0 | Set to 1 to disable Worker thread entirely (edge runtime fallback) |
SF_NBPOST_BATCH_MAX | 50 | Maximum items queued before forced flush |
SF_MAIN_BATCH_SIZE | 0 | Main-thread batch size: 0 (microtask batch), 1 (per-item) |
SF_WORKER_MAX_CONCURRENT | 10 | Max concurrent HTTP requests from Worker to backend. Prevents overload at high volume |
Edge Runtime Limitations
On platforms that do not support Node.js worker_threads, the SDK automatically falls back to sending telemetry via direct fetch() on the main thread. This is detected at runtime — no configuration needed. The fallback works correctly but has higher overhead under heavy logging workloads.
Platforms without worker_threads support:
| Platform | Runtime | Notes |
|---|---|---|
| Cloudflare Workers | V8 isolates | Lightweight edge compute at CDN PoPs; no Node.js APIs |
| Vercel Edge Functions | V8 (Edge Runtime) | Runs at Vercel's edge network; no worker_threads |
| Netlify Edge Functions | Deno | Deno-based edge runtime at CDN PoPs |
| Deno Deploy | Deno | Global serverless Deno; has Web Workers but not Node.js worker_threads |
| AWS Lambda@Edge | Restricted Node.js | CloudFront-attached Lambda; worker_threads disabled (standard Lambda works fine) |
| Fastly Compute | WASM | WebAssembly-based edge compute; no Node.js |
Edge runtimes are lightweight, globally-distributed compute environments that run at CDN Points of Presence (PoPs) close to end users. They execute small functions (typically < 50ms CPU time) for tasks like request routing, authentication, A/B testing, and response transformation. They trade Node.js API completeness for extremely low latency and global distribution. Full application backends typically run on standard Node.js — not edge runtimes.
Platforms where worker_threads works normally:
- Node.js 12+ (all current LTS versions: 18, 20, 22)
- AWS Lambda (standard), Google Cloud Functions, Azure Functions
- Docker, Kubernetes, ECS, EKS, GKE, AKS
- Any standard Node.js hosting (Railway, Render, Heroku, DigitalOcean App Platform, Fly.io)
Troubleshooting
No logs appearing
- Check the local server: Run
SF Veritas: Start Local Serverif not running - Verify the endpoint: Ensure
apiGraphqlEndpointmatches your server port - Check console output: Look for SF Veritas initialization messages in your terminal
Connection refused errors
- Verify the SF Veritas Desktop App is installed and running
- Check that the local server is running (look for server status in the Desktop App)
- Ensure port 6776 is not blocked by a firewall
High memory usage
- Reduce the sampling rate in
.sailfishconfig - Add paths to
excludePathsto reduce trace volume - Lower
maxEntriesPerSecondto rate limit telemetry
Multi-Service Setup
When running multiple services locally, give each a unique serviceIdentifier:
// user-service/server.ts
if (process.env.NODE_ENV === 'development') {
import('@sailfish-ai/sf-veritas').then(({ setupInterceptors }) => {
setupInterceptors({
apiKey: 'sf-veritas-local',
apiGraphqlEndpoint: 'http://localhost:6776/graphql/',
serviceIdentifier: 'user-service',
serviceVersion: '1.0.0',
});
});
}
// order-service/server.ts
if (process.env.NODE_ENV === 'development') {
import('@sailfish-ai/sf-veritas').then(({ setupInterceptors }) => {
setupInterceptors({
apiKey: 'sf-veritas-local',
apiGraphqlEndpoint: 'http://localhost:6776/graphql/',
serviceIdentifier: 'order-service',
serviceVersion: '1.0.0',
});
});
}
Use the service filter in the Console to switch between services.
How It Works
The NODE_ENV environment variable controls when SF Veritas is active:
| Command | NODE_ENV | SF Veritas |
|---|---|---|
npm run dev | development | ✅ Active |
npm run start (production) | production | ❌ Not loaded |
npm run build | production | ❌ Not included |
Most Node.js frameworks automatically set NODE_ENV=development when you run npm run dev:
- Express: Set via
cross-envor your dev script - NestJS: Set automatically by
nest start --watch - Fastify: Set via
cross-envor your dev script
If your framework doesn't set it automatically, add it to your package.json:
{
"scripts": {
"dev": "NODE_ENV=development ts-node src/server.ts",
"start": "NODE_ENV=production node dist/server.js"
}
}
Enterprise Setup
This section applies to users with a Sailfish Enterprise account. If you're using the Desktop App for local development only, the basic setup above is all you need.
Enterprise users need two configurations — one for local development (Desktop App) and one for staging/production (Sailfish cloud). The SDK should detect which environment it's running in and configure itself accordingly.
Dual-Mode Configuration
// instrumentation.ts or at your app's entry point
import type { SetupConfig } from '@sailfish-ai/sf-veritas';
function getSailfishConfig(): SetupConfig | null {
if (process.env.NODE_ENV === 'development') {
// Local development — send to Desktop App
return {
apiKey: 'sf-veritas-local',
apiGraphqlEndpoint: 'http://localhost:6776/graphql/',
serviceIdentifier: 'my-backend-service',
serviceVersion: '1.0.0',
};
}
if (process.env.SAILFISH_API_KEY) {
// Staging/Production — send to Sailfish cloud
// Do NOT set apiGraphqlEndpoint — the SDK defaults to the cloud endpoint
return {
apiKey: process.env.SAILFISH_API_KEY,
serviceIdentifier: 'my-backend-service',
serviceVersion: '1.0.0',
};
}
return null; // SF Veritas disabled
}
const config = getSailfishConfig();
if (config) {
import('@sailfish-ai/sf-veritas').then(({ setupInterceptors }) => {
setupInterceptors(config);
});
}
Environment Variables for Enterprise
Set these in your staging/production deployment:
# Required — your company's Enterprise API key
SAILFISH_API_KEY=your-enterprise-api-key
# Required — the git commit SHA for this build
GIT_SHA=$(git rev-parse HEAD)
# Optional — override the endpoint (only for local dev, do NOT set in production)
# SAILFISH_GRAPHQL_ENDPOINT=http://localhost:6776/graphql/
GIT_SHA
The GIT_SHA environment variable allows Sailfish to correlate telemetry with specific commits. The SDK reads it automatically from process.env.GIT_SHA (with a fallback to VERCEL_GIT_COMMIT_SHA on Vercel).
Set it in your build pipeline:
# Docker
docker build --build-arg GIT_SHA=$(git rev-parse HEAD) .
# In Dockerfile
ARG GIT_SHA
ENV GIT_SHA=$GIT_SHA
# CI/CD (GitHub Actions)
env:
GIT_SHA: ${{ github.sha }}
URL Override
You can override the GraphQL endpoint in two ways:
- Environment variable:
SAILFISH_GRAPHQL_ENDPOINT - Constructor argument:
apiGraphqlEndpointinsetupInterceptors()
| Environment | Endpoint | How |
|---|---|---|
| Local dev | http://localhost:6776/graphql/ | Set apiGraphqlEndpoint or SAILFISH_GRAPHQL_ENDPOINT |
| Staging/Production | Default (cloud) | Do not set either — the SDK uses https://api-service.sailfishqa.com/graphql/ automatically |