The Skopos NodeJS SDK is the core server-side component that receives, processes, and securely stores analytics data in your PocketBase database. It handles session management, visitor identification, data aggregation, and batching for efficient and reliable event tracking.
Installation
npm install @alphasystem/skopos
Initialization
Initialize the SDK asynchronously using SkoposSDK.init() when your server starts. Store and reuse the single instance throughout your application.
// server.js
import { SkoposSDK } from "@alphasystem/skopos";
import express from "express";
const app = express();
let skopos;
async function startServer() {
try {
skopos = await SkoposSDK.init({
siteId: process.env.SKOPOS_SITE_ID, // Your Website Tracking ID
pocketbaseUrl: process.env.POCKETBASE_URL,
adminEmail: process.env.POCKETBASE_ADMIN_EMAIL,
adminPassword: process.env.POCKETBASE_ADMIN_PASSWORD,
batch: true, // Recommended for production
});
console.log("Skopos SDK initialized successfully.");
} catch (error) {
console.error("Failed to initialize Skopos SDK:", error);
process.exit(1);
}
// ... rest of your server setup
}
startServer();
Configuration Options
The init method accepts the following options:
| Option | Type | Required | Description | Default |
|---|---|---|---|---|
siteId |
string |
Yes | The tracking ID for your website, found on the "Websites" page of your Skopos dashboard. | |
pocketbaseUrl |
string |
Yes | The full URL to your PocketBase instance (e.g., http://127.0.0.1:8090). |
|
adminEmail |
string |
Yes | The email for a PocketBase admin or superuser account. Required for the SDK to write data. | |
adminPassword |
string |
Yes | The password for the PocketBase admin account. | |
batch |
boolean |
No | Set to true to enable event batching for improved performance. Recommended for production. |
false |
batchInterval |
number |
No | The interval in milliseconds to send batched events. | 10000 (10 seconds) |
maxBatchSize |
number |
No | The maximum number of events to queue before flushing, regardless of the interval. | 100 |
sessionTimeoutMs |
number |
No | Duration in milliseconds before a visitor's session is considered expired due to inactivity. | 1800000 (30 minutes) |
jsErrorBatchInterval |
number |
No | The interval in milliseconds to send batched JavaScript error reports. | 300000 (5 minutes) |
debug |
boolean |
No | Set to true to enable verbose debug logging. Error logs are always enabled regardless of this setting. |
false |
Best Practices:
- Store credentials in environment variables, never commit them to version control
- Enable batching in production to reduce database load
- Adjust
batchIntervalbased on your traffic volume (lower for high traffic sites) - Use
debug: trueduring development to troubleshoot issues
Security & Validation
The SDK is designed with security as a priority and automatically performs several checks on incoming data from the client-side script.
- Payload Validation: Incoming data is strictly validated against the expected shape and types. Any payload that does not conform is immediately rejected.
- Domain Enforcement: The SDK retrieves the
domainyou set for the website in the dashboard. It then compares this against the origin of incoming requests. Any event from an unrecognized or mismatched domain is dropped, preventing data spoofing. - Data Sanitization: All data is sanitized before processing. Strings are trimmed and constrained to reasonable lengths, numbers are clamped to valid ranges, and large
customDatablobs are rejected to protect your database. - Dashboard-Controlled Settings: The SDK subscribes to your website's configuration in real-time. Changes made in the dashboard, such as updating the IP blacklist or toggling localhost tracking, are applied instantly without needing a server restart.
Tracking Events
1. Client-Side Events (via API Endpoint)
This is the primary method for tracking events from a user's browser. Create an API endpoint that receives the payload from the client-side script and passes it to the SDK's trackApiEvent method.
Key Points:
- This method is fire-and-forget - it returns immediately and processes in the background
- All validation and sanitization happens automatically
- Events from mismatched domains are rejected
- Bot traffic is automatically filtered out
- IP blacklist and localhost settings are enforced
Example: Express API Route
// In your routes file (e.g., api.js)
import express from "express";
const router = express.Router();
// Middleware to parse JSON (if not already applied globally)
router.use(express.json());
// This endpoint URL should match `data-endpoint` in the client script
router.post("/api/event", (req, res) => {
// trackApiEvent is fire-and-forget and processes in the background.
skopos.trackApiEvent(req, req.body);
// Respond immediately with 204 No Content
res.status(204).send();
});
export default router;
Important: Always respond with a 2xx status code immediately. Don't wait for the SDK to finish processing, as this would slow down the user's browsing experience.
2. Server-Side Events
Use trackServerEvent to record events that happen exclusively on your backend, such as:
- User registration or login
- Subscription or payment processing
- API calls from external services
- Scheduled tasks or cron jobs
- File uploads or exports
Example: Tracking a User Signup
app.post("/auth/register", async (req, res) => {
const { email, password, name } = req.body;
// Your business logic
const newUser = await createUserInDatabase(email, password, name);
// Track the server-side event
skopos.trackServerEvent(
req,
"user_signup", // Descriptive event name
{
plan: "free_tier",
userName: name,
referralSource: req.query.ref
} // Optional custom data
);
res.status(201).json({ message: "User created", userId: newUser.id });
});
Example: Tracking a Payment Webhook
app.post("/webhooks/stripe", async (req, res) => {
const event = req.body;
if (event.type === "payment_intent.succeeded") {
// Track the successful payment
skopos.trackServerEvent(
req,
"payment_completed",
{
amount: event.data.object.amount,
currency: event.data.object.currency,
customerId: event.data.object.customer,
}
);
}
res.json({ received: true });
});
Example: Tracking API Calls
app.get("/api/data/export", authenticate, async (req, res) => {
const data = await generateExportData(req.user.id);
// Track the export event
skopos.trackServerEvent(
req,
"data_export",
{
userId: req.user.id,
format: "csv",
rowCount: data.length,
}
);
res.json(data);
});
3. User Identification
Use the identify method to associate an anonymous visitor with your internal user data. This enables powerful features:
- Track users across multiple sessions and devices
- Link analytics data to your CRM or user database
- Segment users by account properties
- Provide personalized support based on user history
Note on SEO Data: When you add a new website through the dashboard, Skopos automatically triggers a background SEO analysis. This initial scan provides baseline SEO metrics, including recommendations, performance scores, and technical health checks. You can re-run analyses manually from the dashboard at any time. Performance scoring requires a Google PageSpeed Insights API key, which can be securely stored in the dashboard's encrypted API Key Vault (Settings → API Keys) or via the PAGESPEED_API_KEY environment variable.
Note on IP Address Storage: By default, Skopos only stores hashed visitor IDs for privacy. If you enable "Store Raw IP Addresses" in Settings → Privacy & Data Collection, full IP addresses will be stored and displayed in session details. The SDK automatically detects this setting and stores IPs accordingly. No SDK configuration changes are needed.
**When to Call identify():
- After successful user login
- After user registration
- When a user updates their profile
- When you learn new information about a user
Example: Login Handler
app.post("/auth/login", async (req, res) => {
const { email, password } = req.body;
// Authenticate the user
const user = await authenticateUser(email, password);
if (!user) {
return res.status(401).json({ error: "Invalid credentials" });
}
// Identify the visitor as this user
await skopos.identify(req, user.id, {
name: user.name,
email: user.email,
phone: user.phone,
metadata: {
accountTier: user.subscription?.tier || "free",
signupDate: user.createdAt,
isVerified: user.emailVerified,
},
});
res.json({ success: true, user });
});
Example: Registration Handler
app.post("/auth/register", async (req, res) => {
const { email, password, name } = req.body;
// Create the new user
const newUser = await createUser({ email, password, name });
// Identify the visitor as this new user
await skopos.identify(req, newUser.id, {
name: newUser.name,
email: newUser.email,
metadata: {
accountTier: "free",
signupDate: new Date().toISOString(),
},
});
res.status(201).json({ success: true, user: newUser });
});
Example: Profile Update
app.patch("/api/user/profile", authenticate, async (req, res) => {
const updates = req.body;
// Update user in your database
const updatedUser = await updateUser(req.user.id, updates);
// Update the identification data
await skopos.identify(req, req.user.id, {
name: updatedUser.name,
email: updatedUser.email,
phone: updatedUser.phone,
metadata: {
accountTier: updatedUser.subscription?.tier,
lastProfileUpdate: new Date().toISOString(),
},
});
res.json({ success: true, user: updatedUser });
});
Important Notes:
- The
identify()method is async and returns a Promise - If the visitor doesn't exist yet, it will be created automatically
- All fields in
userDataare optional - The
metadatafield can store any JSON-serializable data (max 8KB) - Email addresses are automatically validated and normalized
SDK Version Tracking
The dashboard displays which SDK version is connected to each website.
Automatic Detection:
- The SDK reports its version number during initialization
- No manual configuration required
- Updates automatically when you restart with a new SDK version
Dashboard Display:
- Visible on website cards in the "Manage Websites" page
- Shows "Not connected" if the SDK hasn't reported yet
- Useful for tracking which sites need SDK updates
How it works:
- SDK sends version information when connecting to the dashboard
- Dashboard stores and displays this information per website
- Updates persist until the next SDK connection
Benefits:
- Identify outdated SDK versions at a glance
- Plan SDK upgrades across multiple websites
- Troubleshoot version-specific issues
- Monitor deployment status
Graceful Shutdown
To prevent data loss, you must call the shutdown() method when your application is terminating. This:
- Clears all interval timers
- Flushes any remaining events in the queue
- Flushes JavaScript error reports
- Flushes dashboard summary updates
- Closes the real-time subscription connection
Example: Node.js Process Handlers
async function gracefulShutdown() {
console.log("Shutting down gracefully...");
if (skopos) {
await skopos.shutdown();
console.log("Skopos SDK flushed and shut down.");
}
// Close other resources (database connections, etc.)
// ...
process.exit(0);
}
// Handle Ctrl+C
process.on("SIGINT", gracefulShutdown);
// Handle kill commands
process.on("SIGTERM", gracefulShutdown);
// Handle uncaught exceptions
process.on("uncaughtException", (error) => {
console.error("Uncaught Exception:", error);
gracefulShutdown();
});
Example: Express Server with Proper Shutdown
import express from "express";
import { SkoposSDK } from "@alphasystem/skopos";
const app = express();
let skopos;
let server;
async function startServer() {
try {
// Initialize SDK
skopos = await SkoposSDK.init({
siteId: process.env.SKOPOS_SITE_ID,
pocketbaseUrl: process.env.POCKETBASE_URL,
adminEmail: process.env.POCKETBASE_ADMIN_EMAIL,
adminPassword: process.env.POCKETBASE_ADMIN_PASSWORD,
batch: true,
});
console.log("Skopos SDK initialized successfully.");
// Start HTTP server
server = app.listen(3000, () => {
console.log("Server running on port 3000");
});
} catch (error) {
console.error("Failed to initialize:", error);
process.exit(1);
}
}
async function shutdown() {
console.log("Shutting down gracefully...");
// Stop accepting new connections
if (server) {
server.close(() => {
console.log("HTTP server closed.");
});
}
// Flush SDK data
if (skopos) {
await skopos.shutdown();
console.log("Skopos SDK shut down.");
}
process.exit(0);
}
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
startServer();
Best Practices
1. Environment Variables
Never hardcode credentials. Use environment variables:
// .env file
POCKETBASE_URL=https://pb.example.com
SKOPOS_SITE_ID=abc123xyz
POCKETBASE_ADMIN_EMAIL=admin@example.com
POCKETBASE_ADMIN_PASSWORD=your-secure-password
// In your code
import dotenv from "dotenv";
dotenv.config();
const skopos = await SkoposSDK.init({
siteId: process.env.SKOPOS_SITE_ID,
pocketbaseUrl: process.env.POCKETBASE_URL,
adminEmail: process.env.POCKETBASE_ADMIN_EMAIL,
adminPassword: process.env.POCKETBASE_ADMIN_PASSWORD,
});
2. Enable Batching in Production
Batching significantly reduces database load:
const skopos = await SkoposSDK.init({
// ... other options
batch: true,
batchInterval: 10000, // 10 seconds
maxBatchSize: 100,
});
When to adjust batching:
- High traffic sites: Decrease
batchIntervalto 5000ms - Low traffic sites: Increase to 30000ms to reduce overhead
- Real-time dashboards: Use smaller intervals for fresher data
3. Error Handling
The SDK handles most errors internally, but you should still monitor initialization:
try {
skopos = await SkoposSDK.init(options);
console.log("SDK initialized successfully");
} catch (error) {
console.error("Failed to initialize Skopos SDK:", error);
// Decide if you want to exit or continue without analytics
// process.exit(1); // Critical: exit if analytics is required
// OR
// skopos = null; // Non-critical: continue without analytics
}
4. Avoid Blocking the Response
Never await trackApiEvent or trackServerEvent (except identify):
// ❌ Bad: Blocks the response
app.post("/api/event", async (req, res) => {
await skopos.trackApiEvent(req, req.body); // Don't await!
res.status(204).send();
});
// ✅ Good: Fire and forget
app.post("/api/event", (req, res) => {
skopos.trackApiEvent(req, req.body);
res.status(204).send();
});
// ✅ Also good: identify() should be awaited
app.post("/auth/login", async (req, res) => {
const user = await authenticateUser(req.body);
await skopos.identify(req, user.id, { name: user.name }); // Await is fine here
res.json({ success: true });
});
5. Session Timeout Tuning
The default 30-minute session timeout works for most sites, but you can adjust it:
const skopos = await SkoposSDK.init({
// ... other options
sessionTimeoutMs: 1000 * 60 * 15, // 15 minutes for high-activity sites
// OR
sessionTimeoutMs: 1000 * 60 * 60, // 60 minutes for reading-heavy sites
});
6. Debug Mode for Development
Enable debug logging during development:
const skopos = await SkoposSDK.init({
// ... other options
debug: process.env.NODE_ENV === "development",
});
Troubleshooting
SDK Initialization Fails
Problem: SkoposSDK.init() throws an error.
Common Causes:
- Invalid credentials: Verify
adminEmailandadminPassword - PocketBase not accessible: Check that
pocketbaseUrlis correct and the server is running - Wrong siteId: Ensure the tracking ID matches a website in your dashboard
- Network issues: Check firewall rules and network connectivity
Solution:
try {
const skopos = await SkoposSDK.init({
debug: true, // Enable debug logging
// ... other options
});
} catch (error) {
console.error("Init error:", error.message);
// Check the error message for specific details
}
Events Not Appearing in Dashboard
Problem: Events are sent but don't show up in the dashboard.
Checklist:
- ✅ Verify the SDK is initialized with the correct
siteId - ✅ Check that the website domain matches the origin of incoming requests
- ✅ Ensure the client-side script has the correct
data-endpoint - ✅ Look for validation errors in your server logs (enable
debug: true) - ✅ Check if the IP is in the blacklist
- ✅ Verify localhost tracking isn't disabled (if testing locally)
- ✅ Confirm PocketBase collections are accessible
Debug Command:
// Enable debug mode temporarily
skopos.debug = true;
High Memory Usage
Problem: The SDK is consuming too much memory.
Causes:
- Event queue growing too large (batch size too high)
- Session cache not cleaning up properly
- Too many pending events
Solutions:
const skopos = await SkoposSDK.init({
batch: true,
maxBatchSize: 50, // Reduce from default 100
batchInterval: 5000, // Flush more frequently
sessionTimeoutMs: 1000 * 60 * 15, // Reduce session lifetime
});
// Manually flush if needed
await skopos.flushEvents();
Authentication Expired Errors
Problem: SDK logs "Admin token expired" errors.
Explanation: The SDK automatically re-authenticates when tokens expire. However, if re-authentication fails:
Solutions:
- Verify admin credentials are still valid
- Check PocketBase admin password hasn't changed
- Ensure PocketBase is accessible from your server
- Check for network connectivity issues
Events Being Rejected
Problem: Events are being dropped with "domain mismatch" warnings.
Cause: The SDK validates that incoming events originate from your configured domain.
Solution:
- Go to the Websites page in your dashboard
- Verify the domain is correctly set (e.g.,
example.com, nothttps://example.com) - The SDK strips
www.and compares hostnames, sowww.example.comandexample.comare treated as the same
Real-Time Updates Not Working
Problem: Dashboard settings changes don't apply to the SDK.
Causes:
- WebSocket connection failed
- PocketBase real-time not enabled
- Network firewall blocking WebSocket connections
Solution:
- Check PocketBase logs for WebSocket errors
- Verify your server can establish WebSocket connections to PocketBase
- Check firewall rules for outbound WebSocket connections
- Restart the SDK (it re-establishes the connection on init)
Performance Optimization
High-Traffic Scenarios
For sites with millions of page views:
const skopos = await SkoposSDK.init({
batch: true,
batchInterval: 5000, // Flush every 5 seconds
maxBatchSize: 200, // Larger batches
jsErrorBatchInterval: 60000, // Only flush errors every minute
});
Low-Traffic Scenarios
For sites with minimal traffic:
const skopos = await SkoposSDK.init({
batch: false, // Disable batching, send immediately
});
Database Optimization
- Enable data retention: Set appropriate retention periods to prevent unbounded database growth
- Monitor collection sizes: Regularly check your PocketBase database size
- Use indexes: PocketBase automatically indexes key fields like
visitorId,sessionId, etc.
Advanced Usage
Multiple Websites from One SDK Instance
You can track multiple websites with a single SDK instance using the optional siteId parameter:
// Initialize with default site
const skopos = await SkoposSDK.init({
siteId: "main-site-id",
// ... other options
});
// Track to default site
skopos.trackServerEvent(req, "event_name");
// Track to a different site
skopos.trackServerEvent(req, "event_name", {}, "other-site-id");
Custom Visitor Identification
The SDK generates visitor IDs by hashing IP + User-Agent + Site ID. If you need custom visitor identification logic, you'll need to modify the SDK source code (it's open source!).
Accessing Raw PocketBase
If you need direct access to PocketBase for custom queries:
// Not officially supported, but possible:
// skopos.pb gives you access to the PocketBase client
// Use with caution as this bypasses SDK logic
API Reference Summary
| Method | Parameters | Returns | Description |
|---|---|---|---|
SkoposSDK.init(options) |
SkoposSDKOptions |
Promise<SkoposSDK> |
Initialize and authenticate the SDK |
trackApiEvent(req, payload) |
IncomingMessage, ApiEventPayload |
void |
Track client-side events (fire-and-forget) |
trackServerEvent(req, name, data?, siteId?) |
IncomingMessage, string, object, string |
void |
Track backend events (fire-and-forget) |
identify(req, userId, userData?) |
IncomingMessage, string, IdentifyData |
Promise<void> |
Associate visitor with user data |
flushEvents() |
- | Promise<void> |
Manually flush event queue |
shutdown() |
- | Promise<void> |
Gracefully shut down SDK |