TraceKey is a self-hosted interaction-tracking and analytics platform for websites and browser-based applications. It gives every project a public API key, accepts events from the TraceKey JavaScript SDK or HTTP API, enriches them with useful visitor context, and stores the results in PostgreSQL.
The included dashboard is a useful starting point, not a limitation. TraceKey is designed around accessible event data: record the actions and custom JSON your application cares about, then build your own operational dashboard, product analytics view, queue monitor, funnel, or reporting workflow around that data.
TraceKey can run on a small server with a minimum of 1 vCPU and 1 GB RAM. For a public deployment with sustained traffic, 2 GB RAM or more is recommended so the application, PostgreSQL, and container builds have comfortable headroom.
- Tracks page landings, button clicks, heartbeats, exits, queue activity, and custom events.
- Assigns a persistent device ID so repeat activity can be grouped without requiring a signed-in end user.
- Captures the current route, timestamp, referrer, IP address, browser user agent, device type, region, and supported device details.
- Accepts arbitrary application-specific JSON through
additionalInfo. - Separates data into projects with individual API keys.
- Supports multiple users per project.
- Provides project activity tables, date filtering, visitor totals, top-region statistics, and customer dashboard metrics.
- Keeps the raw PostgreSQL event data available for custom dashboards, SQL reports, APIs, exports, and integrations.
- Ships as a single Next.js application with PostgreSQL, making it practical to self-host on modest infrastructure.
Your application
|
| TraceKey SDK or POST /api/v1/events
v
TraceKey ingestion API
|
| validates project API key and enriches the event
v
PostgreSQL interactions table
|
+--> Built-in TraceKey dashboard
+--> Your own dashboard or API
+--> SQL reports, exports, and automations
Each interaction can contain standard analytics fields and your own domain-specific data. For example, an attraction, venue, or appointment application could attach a party size, ride identifier, booking source, or queue duration:
{
"api_key": "YOUR_TRACEKEY_API_KEY",
"device_id": "customer-device-id",
"page_route": "/rides/coaster",
"event_name": "join_queue",
"additionalInfo": {
"memberCount": 4,
"rideId": "coaster-01",
"estimatedWaitMinutes": 25
}
}Because additional_info is stored as PostgreSQL jsonb, you can add application-specific fields without creating a new column for every event property.
- Next.js 16 and React 19
- TypeScript
- PostgreSQL
- Tailwind CSS and shadcn/ui
- Official
tracekey-sdk - Docker and Docker Compose
- Optional Cloudflare Tunnel
For the recommended Docker installation:
- A Linux server, VM, or local machine
- Docker Engine with the Docker Compose plugin
- 1 vCPU and 1 GB RAM minimum
- Approximately 2 GB of free disk space for images, dependencies, and initial database storage
- Additional persistent storage based on event volume and retention
For development without Docker:
- Node.js 20 or newer
- npm
- PostgreSQL 16 or newer
The included docker-compose.yml defines:
| Service | Purpose | Host access |
|---|---|---|
app |
Builds and runs the Next.js application | http://localhost:3101 |
db |
PostgreSQL database with persistent storage | localhost:5433 |
cloudflared |
Optional public Cloudflare Tunnel | Configured tunnel hostname |
git clone https://github.com/joel909/TraceKey.git
cd TraceKeyThe cloudflared service is optional, but when you want a public HTTPS address its tunnel token should be configured before the application stack is started. Do not add the published-application route yet; that comes after Docker is running.
- Open the Cloudflare dashboard.
- Go to Networking → Tunnels, select Create Tunnel, and create a Cloudflared tunnel.
- Choose the Docker setup option and copy the generated tunnel token from the installation command.
- Put that token in the
cloudflaredservice indocker-compose.yml.
The relevant Compose configuration should resemble:
cloudflared:
image: cloudflare/cloudflared:latest
restart: unless-stopped
command: tunnel --no-autoupdate run --token YOUR_CLOUDFLARE_TUNNEL_TOKEN
networks:
- app-networkIf you only need local or private-network access, remove or comment out the entire cloudflared service before continuing.
Do not commit a real tunnel token to a public repository. If a token has already been exposed, rotate it in Cloudflare before deploying.
Before starting the stack, open docker-compose.yml and replace the deployment-specific values:
- Change
POSTGRES_PASSWORDto a strong database password. - Put the same password in the
POSTGRES_URLused by theappservice.
The database values must continue to match. A typical configuration looks like this:
db:
environment:
POSTGRES_USER: tracekey_db_client
POSTGRES_PASSWORD: replace-with-a-strong-password
POSTGRES_DB: tracekey
app:
environment:
POSTGRES_URL: postgresql://tracekey_db_client:replace-with-a-strong-password@db:5432/tracekeyDo not commit real production database credentials to a public repository.
docker compose up -d --buildThe first build installs dependencies and creates the production Next.js bundle, so it takes longer than later starts.
Check the service state:
docker compose psFollow the logs if anything fails:
docker compose logs -f cloudflared app dbAfter the Docker build finishes, check the tunnel container:
docker compose logs -f cloudflaredWait until the logs show that the tunnel is registered and connected. Then press Ctrl+C to stop following the logs; this does not stop the container.
You can also return to Networking → Tunnels in Cloudflare and confirm that the tunnel status is Healthy. If you removed the optional cloudflared service, skip this and the next step.
Once cloudflared is connected and the app container is running:
- Open Networking → Tunnels in the Cloudflare dashboard and select your tunnel.
- Select Routes → Add route → Published application.
- Enter the subdomain and domain you want to use, such as
tracekey.example.com. - Leave Path empty to route every TraceKey page and API endpoint through the tunnel.
- Set Service URL to
http://app:3000exactly. - Select Add route and wait for Cloudflare to configure the hostname.
Use app:3000 because containers communicate through the Compose service name and internal application port. Do not use localhost:3101 here: port 3101 is only the host-side mapping and localhost inside the tunnel container would refer to the tunnel container itself.
Open the HTTPS hostname configured in Cloudflare. For a local check, you can also visit http://localhost:3101. Create an account, then create or open a project. TraceKey creates an API key for each project. From the project page, select Setup with API Key for SDK installation and integration examples.
docker compose stop
docker compose startTo stop and remove the containers while preserving the database:
docker compose downThe named pgdata volume holds PostgreSQL data between container replacements.
Running
docker compose down -valso deletes the database volume and all TraceKey data. Use it only when you intentionally want a completely fresh installation.
Docker mounts db/schema.sql into PostgreSQL's initialization directory. PostgreSQL runs this file automatically when the pgdata volume is created for the first time. It creates the extension, tables, relationships, constraints, and indexes required by TraceKey.
Initialization scripts do not run again against an existing volume. If you edit the schema after the first start, apply the migration manually or recreate the volume only if losing the existing data is acceptable.
The main tables are:
| Table | Purpose |
|---|---|
users |
TraceKey accounts and authentication data |
projects |
Project settings and API keys |
user_projects |
Many-to-many project membership |
interactions |
Event, visitor, device, route, region, and custom JSON data |
npm installCreate a database and apply the schema:
psql "postgresql://USER:PASSWORD@localhost:5432/tracekey" -f db/schema.sqlCreate .env in the repository root:
POSTGRES_URL=postgresql://USER:PASSWORD@localhost:5432/tracekeynpm run devOpen http://localhost:3000.
Useful checks:
npm run lint
npm run buildInstall the SDK in the application you want to track:
npm install tracekey-sdkFor a Next.js client, place the public project key in .env.local:
NEXT_PUBLIC_TRACEKEY_API_KEY=YOUR_TRACEKEY_API_KEYCreate one shared client:
// src/lib/tracekey.ts
import { TracekeyClient } from "tracekey-sdk";
export const tracekey = new TracekeyClient({
apiKey: process.env.NEXT_PUBLIC_TRACEKEY_API_KEY!,
});Log events from client components, effects, or browser event handlers:
"use client";
import { useEffect } from "react";
import { tracekey } from "@/lib/tracekey";
export function CheckoutButton() {
useEffect(() => {
void tracekey.logLandingEvent();
}, []);
return (
<button onClick={() => tracekey.logButtonClickEvent("checkout")}>
Checkout
</button>
);
}The SDK also supports heartbeat, exit, custom, queue-join, and boarded events. See the SDK repository for its current API.
Applications can post directly to the ingestion endpoint when they need a custom payload:
curl -X POST http://localhost:3101/api/v1/events \
-H "Content-Type: application/json" \
-d '{
"api_key": "YOUR_TRACEKEY_API_KEY",
"device_id": "example-device-id",
"page_route": "/checkout",
"event_name": "order_started",
"additionalInfo": {
"cartItems": 3,
"cartValue": 49.99,
"currency": "USD"
}
}'The project API key identifies where the event belongs. Treat it as a public project identifier: it is suitable for browser use, but it should not grant access to private dashboard data or administrative actions.
TraceKey intentionally keeps event records in a queryable format. You can build custom views in three common ways:
- Extend the Next.js application with a new authenticated API route and dashboard page.
- Connect a trusted visualization tool directly to a read-only PostgreSQL account.
- Export or aggregate interaction data into a warehouse or reporting service.
For example, custom JSON values can be grouped directly in PostgreSQL:
SELECT
additional_info->>'rideId' AS ride_id,
COUNT(*) AS groups_joined,
SUM((additional_info->>'memberCount')::integer) AS guests_joined
FROM interactions
WHERE api_key = 'YOUR_TRACEKEY_API_KEY'
AND action_name = 'join_queue'
GROUP BY additional_info->>'rideId'
ORDER BY guests_joined DESC;For public or third-party dashboards, do not expose the database owner credentials. Use a restricted read-only database role or place an authenticated API between the dashboard and PostgreSQL.
The 1 vCPU / 1 GB RAM minimum is appropriate for development, evaluation, and low-volume self-hosting. Actual capacity depends on event rate, dashboard query complexity, retention, and how many services share the machine.
For larger installations:
- Increase RAM before tuning aggressively; PostgreSQL and Node.js both benefit from memory headroom.
- Keep the PostgreSQL volume on persistent SSD storage.
- Add retention or archival policies for old interaction records.
- Monitor disk growth, container restarts, memory use, and slow queries.
- Add indexes for frequently queried custom fields or create summary tables for expensive dashboards.
- Run PostgreSQL separately when ingestion traffic or reporting load becomes significant.
Before using TraceKey for real user data:
- Replace all example/default database credentials.
- Remove and rotate any committed Cloudflare tunnel token.
- Put the application behind HTTPS.
- Restrict database network exposure; port
5433does not need to be public. - Use a read-only database role for external reporting tools.
- Add automated PostgreSQL backups and test restoration.
- Add monitoring, retention rules, and rate limiting appropriate to your traffic.
- Review authentication before Internet-facing use. The current codebase should be upgraded to hash account passwords rather than storing or comparing plaintext passwords.
- Review CORS policy and narrow allowed origins when the set of tracked applications is known.
- Collect only data you need, disclose tracking to users, and follow the privacy and consent requirements that apply in your jurisdiction.
src/app/ Next.js pages and API routes
src/components/ Dashboard and reusable UI components
src/lib/controllers/ Application and request orchestration
src/lib/database/ Queries, services, and PostgreSQL access
src/lib/errors/ Centralized application errors
db/schema.sql Docker database initialization schema
Dockerfile Production application image
docker-compose.yml App, PostgreSQL, and tunnel services
documentation/ Additional internal documentation
Confirm that the db service is healthy and that the app uses db:5432, not localhost, inside Compose:
docker compose ps
docker compose logs db appThe initialization SQL only runs for a new PostgreSQL volume. Apply the schema change manually or, for disposable development data only, recreate the volume.
Change the host side of the appropriate mapping in docker-compose.yml. For example, 3200:3000 exposes the application on port 3200 without changing its internal port.
Confirm that the token is current, the tunnel hostname targets http://app:3000, and both services are attached to app-network.
Check that the API key belongs to the expected project, the request reaches /api/v1/events, and the browser console or app logs do not show CORS or network errors. Then inspect:
docker compose logs -f app dbIssues and pull requests are welcome. When changing the database, include an explicit migration plan for installations that already have a populated pgdata volume. Run lint and a production build before submitting changes:
npm run lint
npm run build