Willkommen zu meinem umfassenden Leitfaden für die lokale Bereitstellung eines Replay-Servers nach dem "Tardis Machine"-Prinzip. In diesem Tutorial zeige ich Ihnen Schritt für Schritt, wie Sie einen eigenen WebSocket- und HTTP-standardisierten Server aufsetzen, der historische Daten in Echtzeit zurückspielen kann. Die Anleitung richtet sich an absolute Anfänger ohne Vorkenntnisse – keine Sorge, ich erkläre jeden Begriff verständlich.

Was ist eine Tardis Machine und wofür braucht man sie?

Stellen Sie sich vor, Sie könnten die Zeit zurückdrehen und vergangene API-Antworten nochmal abrufen – genau das macht ein Replay-Server. Die "Tardis Machine" ist ein System, das alle eingehenden Anfragen und ausgehenden Antworten protokolliert und diese später 1:1 reproduzieren kann. Das ist besonders nützlich für:

Voraussetzungen für die Installation

Bevor wir beginnen, benötigen Sie folgende Software auf Ihrem Computer:

💡 Hinweis für Anfänger: Installieren Sie Node.js am einfachsten über die offizielle Website nodejs.org. Der Download-Button erkennt Ihr Betriebssystem automatisch.

Schritt 1: Projektstruktur erstellen

Erstellen Sie einen neuen Ordner für Ihr Projekt und navigieren Sie hinein:

mkdir tardis-machine
cd tardis-machine
npm init -y

Dies erstellt eine neue Node.js-Anwendung mit Standardkonfiguration. Die Antwort sollte etwa so aussehen:

{
  "name": "tardis-machine",
  "version": "1.0.0",
  "description": "WS/HTTP replay server for historical data",
  "main": "src/server.js",
  "scripts": {
    "start": "node src/server.js",
    "dev": "nodemon src/server.js"
  },
  "dependencies": {}
}

Schritt 2: Abhängigkeiten installieren

Jetzt installieren wir die notwendigen Pakete. Diese Bibliotheken sind das Herzstück unseres Replay-Servers:

npm install express ws sqlite3 uuid cors dotenv
npm install --save-dev nodemon

Nach erfolgreicher Installation sehen Sie eine Bestätigung mit den installierten Versionen, z.B.:

added 127 packages in 12.3s
added 45 packages in 8.7s

Schritt 3: Datenbankschema erstellen

Erstellen Sie im Hauptverzeichnis die Datei database.js. Diese verwaltet die SQLite-Datenbank, die alle Ihre replayspeichert:

const Database = require('better-sqlite3');
const path = require('path');

const db = new Database(path.join(__dirname, 'replays.db'));

// Tabelle für API-Requests erstellen
db.exec(`
  CREATE TABLE IF NOT EXISTS requests (
    id TEXT PRIMARY KEY,
    endpoint TEXT NOT NULL,
    method TEXT NOT NULL,
    headers TEXT NOT NULL,
    body TEXT,
    query_params TEXT,
    timestamp INTEGER NOT NULL,
    response_status INTEGER,
    response_headers TEXT,
    response_body TEXT,
    response_time_ms INTEGER
  );

  CREATE INDEX IF NOT EXISTS idx_timestamp ON requests(timestamp);
  CREATE INDEX IF NOT EXISTS idx_endpoint ON requests(endpoint);
`);

module.exports = db;

Schritt 4: WebSocket-Handler implementieren

Die Datei websocket-handler.js verarbeitet Echtzeit-Verbindungen. WebSocket ist perfekt für bidirektionale Kommunikation mit minimaler Latenz:

const WebSocket = require('ws');
const { v4: uuidv4 } = require('uuid');
const db = require('./database');

class WebSocketHandler {
  constructor(server) {
    this.wss = new WebSocket.Server({ server });
    this.clients = new Map();
    this.recording = true;

    this.wss.on('connection', (ws, req) => {
      const clientId = uuidv4();
      this.clients.set(clientId, { ws, connectedAt: Date.now() });

      console.log([WS] Client verbunden: ${clientId});

      ws.on('message', (message) => this.handleMessage(clientId, message));
      ws.on('close', () => this.handleDisconnect(clientId));
      ws.on('error', (error) => this.handleError(clientId, error));
    });
  }

  handleMessage(clientId, rawMessage) {
    try {
      const data = JSON.parse(rawMessage.toString());

      // Request aufzeichnen
      if (this.recording && data.type === 'request') {
        const requestId = uuidv4();
        const timestamp = Date.now();

        db.prepare(`
          INSERT INTO requests (id, endpoint, method, headers, body, query_params, timestamp)
          VALUES (?, ?, ?, ?, ?, ?, ?)
        `).run(
          requestId,
          data.endpoint,
          data.method || 'GET',
          JSON.stringify(data.headers || {}),
          JSON.stringify(data.body || null),
          JSON.stringify(data.query || {}),
          timestamp
        );

        this.clients.get(clientId).ws.send(JSON.stringify({
          type: 'recorded',
          requestId,
          timestamp
        }));
      }

      // Replay-Antwort senden
      if (data.type === 'replay') {
        this.replayRequest(clientId, data.requestId);
      }
    } catch (error) {
      console.error('[WS] Nachrichtenfehler:', error.message);
      this.clients.get(clientId)?.ws.send(JSON.stringify({
        type: 'error',
        message: 'Ungültiges Nachrichtenformat'
      }));
    }
  }

  async replayRequest(clientId, requestId) {
    const request = db.prepare('SELECT * FROM requests WHERE id = ?').get(requestId);

    if (!request) {
      this.clients.get(clientId).ws.send(JSON.stringify({
        type: 'error',
        message: Request ${requestId} nicht gefunden
      }));
      return;
    }

    // Simuliere Antwort mit minimaler Verzögerung (0-50ms)
    await new Promise(r => setTimeout(r, Math.random() * 50));

    this.clients.get(clientId).ws.send(JSON.stringify({
      type: 'response',
      requestId,
      status: request.response_status,
      headers: JSON.parse(request.response_headers || '{}'),
      body: JSON.parse(request.response_body || '{}'),
      responseTime: request.response_time_ms
    }));
  }

  handleDisconnect(clientId) {
    console.log([WS] Client getrennt: ${clientId});
    this.clients.delete(clientId);
  }

  handleError(clientId, error) {
    console.error([WS] Fehler bei Client ${clientId}:, error.message);
    this.clients.delete(clientId);
  }

  setRecording(enabled) {
    this.recording = enabled;
    console.log([WS] Aufzeichnung: ${enabled ? 'aktiviert' : 'deaktiviert'});
  }
}

module.exports = WebSocketHandler;

Schritt 5: HTTP-Proxy-Server erstellen

Der HTTP-Server verarbeitet REST-API-Anfragen und leitet sie entweder an einen Backend-Service weiter oder spielt gespeicherte Antworten zurück:

const express = require('express');
const cors = require('cors');
const { v4: uuidv4 } = require('uuid');
const db = require('./database');

function createHttpServer(options = {}) {
  const app = express();
  app.use(cors());
  app.use(express.json());

  // Middleware: Request-Logger und Recorder
  app.use((req, res, next) => {
    const requestId = uuidv4();
    const startTime = Date.now();
    const timestamp = startTime;

    console.log([HTTP] ${req.method} ${req.path});

    // Response abfangen
    const originalSend = res.send;
    res.send = function(body) {
      const responseTime = Date.now() - startTime;

      // Nur aufzeichnen wenn aktiv
      if (options.recording !== false) {
        try {
          db.prepare(`
            INSERT INTO requests 
            (id, endpoint, method, headers, body, query_params, timestamp, 
             response_status, response_headers, response_body, response_time_ms)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
          `).run(
            requestId,
            req.path,
            req.method,
            JSON.stringify(req.headers),
            JSON.stringify(req.body),
            JSON.stringify(req.query),
            timestamp,
            res.statusCode,
            JSON.stringify(res.getHeaders()),
            typeof body === 'string' ? body : JSON.stringify(body),
            responseTime
          );
        } catch (err) {
          console.error('[HTTP] Aufzeichnungsfehler:', err.message);
        }
      }

      // Request-ID in Response-Header
      res.setHeader('X-Request-ID', requestId);
      res.setHeader('X-Response-Time', ${responseTime}ms);

      return originalSend.call(this, body);
    };

    next();
  });

  // Replay-Endpoint: Hole gespeicherte Antwort
  app.get('/replay/:requestId', (req, res) => {
    const request = db.prepare('SELECT * FROM requests WHERE id = ?').get(req.params.requestId);

    if (!request) {
      return res.status(404).json({ error: 'Request nicht gefunden' });
    }

    res.status(request.response_status);
    Object.entries(JSON.parse(request.response_headers || '{}')).forEach(([k, v]) => {
      if (!['content-type', 'content-length'].includes(k.toLowerCase())) {
        res.setHeader(k, v);
      }
    });

    res.json(JSON.parse(request.response_body || '{}'));
  });

  // Replay-Endpoint: Suche nach Endpoint und Zeitraum
  app.post('/replay/search', (req, res) => {
    const { endpoint, method, startTime, endTime, limit = 100 } = req.body;

    let query = 'SELECT * FROM requests WHERE 1=1';
    const params = [];

    if (endpoint) {
      query += ' AND endpoint LIKE ?';
      params.push(%${endpoint}%);
    }
    if (method) {
      query += ' AND method = ?';
      params.push(method);
    }
    if (startTime) {
      query += ' AND timestamp >= ?';
      params.push(startTime);
    }
    if (endTime) {
      query += ' AND timestamp <= ?';
      params.push(endTime);
    }

    query += ' ORDER BY timestamp DESC LIMIT ?';
    params.push(limit);

    const results = db.prepare(query).all(...params);
    res.json({ count: results.length, requests: results });
  });

  // Statistik-Endpoint
  app.get('/stats', (req, res) => {
    const total = db.prepare('SELECT COUNT(*) as count FROM requests').get();
    const avgResponse = db.prepare('SELECT AVG(response_time_ms) as avg FROM requests').get();
    const byMethod = db.prepare(`
      SELECT method, COUNT(*) as count FROM requests GROUP BY method
    `).all();

    res.json({
      total_requests: total.count,
      avg_response_time_ms: Math.round(avgResponse.avg || 0),
      by_method: byMethod
    });
  });

  return app;
}

module.exports = createHttpServer;

Schritt 6: Hauptserver zusammenführen

Die zentrale Datei server.js verbindet HTTP und WebSocket zu einem unified Replay-Server:

require('dotenv').config();
const http = require('http');
const createHttpServer = require('./http-server');
const WebSocketHandler = require('./websocket-handler');

const PORT = process.env.PORT || 3000;
const BACKEND_URL = process.env.BACKEND_URL || 'https://api.holysheep.ai/v1';

// HTTP-Server erstellen
const app = createHttpServer({ recording: true });
const server = http.createServer(app);

// WebSocket-Server anhängen
const wsHandler = new WebSocketHandler(server);

// Admin-Endpunkte
app.get('/admin/status', (req, res) => {
  res.json({
    status: 'running',
    ws_clients: wsHandler.clients.size,
    recording: wsHandler.recording,
    backend_url: BACKEND_URL
  });
});

app.post('/admin/recording', (req, res) => {
  const { enabled } = req.body;
  wsHandler.setRecording(enabled !== false);
  res.json({ recording: wsHandler.recording });
});

app.delete('/admin/cache', (req, res) => {
  const db = require('./database');
  const result = db.prepare('DELETE FROM requests').run();
  res.json({ deleted: result.changes });
});

// Server starten
server.listen(PORT, () => {
  console.log(`
╔═══════════════════════════════════════════════════════════╗
║           🚀 Tardis Machine Server gestartet              ║
╠═══════════════════════════════════════════════════════════╣
║  HTTP:  http://localhost:${PORT}                            ║
║  WS:    ws://localhost:${PORT}                              ║
║  Admin: http://localhost:${PORT}/admin/status                ║
╚═══════════════════════════════════════════════════════════╝
  `);
});

Schritt 7: Client-Konfiguration

Erstellen Sie eine .env-Datei für Ihre Konfiguration:

PORT=3000
BACKEND_URL=https://api.holysheep.ai/v1
RECORDING_ENABLED=true

Der folgende Client-Code demonstriert, wie Sie Anfragen durch den Replay-Server leiten und gleichzeitig aufzeichnen:

class TardisClient {
  constructor(options = {}) {
    this.baseUrl = options.baseUrl || 'http://localhost:3000';
    this.backendUrl = options.backendUrl || process.env.BACKEND_URL;
    this.recording = options.recording !== false;
    this.ws = null;
  }

  // WebSocket-Verbindung herstellen
  connect() {
    return new Promise((resolve, reject) => {
      this.ws = new WebSocket(ws://localhost:3000);

      this.ws.onopen = () => {
        console.log('[Client] WebSocket verbunden');
        resolve();
      };

      this.ws.onerror = (error) => {
        console.error('[Client] WebSocket-Fehler:', error);
        reject(error);
      };

      this.ws.onmessage = (event) => {
        const data = JSON.parse(event.data);
        this.handleMessage(data);
      };
    });
  }

  // HTTP-Request senden
  async request(endpoint, options = {}) {
    const fullUrl = ${this.backendUrl}${endpoint};
    const startTime = Date.now();

    try {
      const response = await fetch(fullUrl, {
        method: options.method || 'GET',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': Bearer ${options.apiKey || process.env.HOLYSHEEP_API_KEY},
          ...options.headers
        },
        body: options.body ? JSON.stringify(options.body) : undefined
      });

      const responseTime = Date.now() - startTime;
      const data = await response.json();

      if (this.recording) {
        // Request lokal aufzeichnen
        await this.recordRequest({
          endpoint,
          method: options.method || 'GET',
          headers: options.headers || {},
          body: options.body,
          query: options.query || {},
          response: {
            status: response.status,
            headers: {},
            body: data,
            time: responseTime
          }
        });
      }

      return { success: true, data, responseTime };
    } catch (error) {
      return { success: false, error: error.message };
    }
  }

  // Request aufzeichnen
  async recordRequest(data) {
    return fetch(${this.baseUrl}/record, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });
  }

  // Replay-Antwort abrufen
  async replay(requestId) {
    const response = await fetch(${this.baseUrl}/replay/${requestId});
    return response.json();
  }

  handleMessage(data) {
    console.log('[Client] Server-Nachricht:', data.type);
  }

  disconnect() {
    if (this.ws) {
      this.ws.close();
      this.ws = null;
    }
  }
}

// Verwendung
const client = new TardisClient({
  baseUrl: 'http://localhost:3000',
  backendUrl: 'https://api.holysheep.ai/v1'
});

await client.connect();

// Beispiel: Chat-Completion request
const result = await client.request('/chat/completions', {
  method: 'POST',
  apiKey: 'YOUR_HOLYSHEEP_API_KEY',
  body: {
    model: 'gpt-4.1',
    messages: [{ role: 'user', content: 'Hallo Welt!' }]
  }
});

console.log('Antwort:', result.data);
client.disconnect();

Schritt 8: Server testen und starten

Starten Sie den Server mit dem Befehl:

npm start

Sie sollten die Willkommensnachricht sehen. Öffnen Sie einen zweiten Terminal und testen Sie die API:

# Server-Status prüfen
curl http://localhost:3000/admin/status

Statistiken abrufen

curl http://localhost:3000/stats

Request suchen

curl -X POST http://localhost:3000/replay/search \ -H "Content-Type: application/json" \ -d '{"endpoint": "/chat", "limit": 10}'

Häufige Fehler und Lösungen

Fehler 1: "ECONNREFUSED" beim Serverstart

Problem: Der Server kann nicht starten, Port bereits belegt.

Error: listen EADDRINUSE :::3000

Lösung: Ändern Sie den Port in der .env-Datei:

PORT=4000

Oder beenden Sie den Prozess auf Port 3000:

Windows:

netstat -ano | findstr :3000 taskkill /PID [PID-NUMMER] /F

macOS/Linux:

lsof -i :3000 kill -9 [PID]

Fehler 2: "SyntaxError: Unexpected token 'export'"

Problem: Node.js-Version unterstützt keine ES-Module.

SyntaxError: Cannot use import statement outside a module

Lösung: Fügen Sie "type": "module" in package.json hinzu oder verwenden Sie CommonJS-Syntax:

// Ändern Sie in package.json:
{
  "type": "module"
}

// Oder verwenden Sie require statt import:
const express = require('express');
const cors = require('cors');

Fehler 3: "WebSocket connection failed"

Problem: Browser oder Client kann keine WebSocket-Verbindung aufbauen.

WebSocket connection to 'ws://localhost:3000' failed

Lösung: Prüfen Sie die Firewall und CORS-Einstellungen:

// In server.js CORS konfigurieren:
app.use(cors({
  origin: '*', // Für Entwicklung
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

// Für Produktion spezifische Domains:
app.use(cors({
  origin: ['https://ihre-domain.com', 'https://www.ihre-domain.com'],
  credentials: true
}));

Fehler 4: "Database is locked"

Problem: SQLite-Datenbank wird von mehreren Prozessen gleichzeitig beschrieben.

SQLITE_BUSY: database is locked

Lösung: Verwenden Sie Write-Ahead Logging (WAL) für bessere Parallelität:

// In database.js:
const db = new Database(path.join(__dirname, 'replays.db'));

// WAL-Modus aktivieren für bessere Concurrency
db.pragma('journal_mode = WAL');
db.pragma('busy_timeout = 5000');

// Prepared Statements mit Timeout
const insertStmt = db.prepare(`
  INSERT INTO requests ...
`).extend({ busyTimeout: 5000 });

Fehler 5: "Invalid JSON in request body"

Problem: Client sendet ungültige JSON-Daten.

Lösung: Validieren Sie JSON vor dem Senden:

function safeJsonStringify(obj) {
  try {
    return JSON.stringify(obj);
  } catch (e) {
    console.error('JSON-Serialisierungsfehler:', e);
    return JSON.stringify({ error: 'Invalid data', raw: String(obj) });
  }
}

function safeJsonParse(str) {
  try {
    return JSON.parse(str);
  } catch (e) {
    console.error('JSON-Parsingfehler:', str);
    return null;
  }
}

// Verwendung:
const safeBody = safeJsonParse(rawBody);
if (!safeBody) {
  return res.status(400).json({ error: 'Ungültiges JSON-Format' });
}

Skalierung und Produktions deployment

Für den Produktiveinsatz empfehle ich Docker-Containerisierung. Erstellen Sie eine Dockerfile:

FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

EXPOSE 3000

CMD ["node", "server.js"]

Und eine docker-compose.yml für PostgreSQL statt SQLite:

version: '3.8'
services:
  tardis:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://user:pass@postgres:5432/tardis
      - BACKEND_URL=https://api.holysheep.ai/v1
    depends_on:
      - postgres

  postgres:
    image: postgres:16-alpine
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
      - POSTGRES_DB=tardis
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Geeignet / nicht geeignet für

✅ Perfekt geeignet für
Entwickler-TeamsLokale Tests ohne API-Kosten
CI/CD-PipelinesReproduzierbare Integrationstests
PrototypingSchnelle Entwicklung ohne Backend-Abhängigkeiten
SchulungenDemo-Umgebungen mit realistischen Daten
Offline-EntwicklungArbeit ohne Internetverbindung
❌ Nicht geeignet für
Echtzeit-ProduktionLive-Systeme mit frischem API-Zugriff
Skalierung >1000 req/sSQLite-Limitierungen bei hoher Last
Team-weite RepositoriesOhne zusätzliche Infrastructure
Langfristige DatenspeicherungPostgreSQL wäre besser geeignet

Preise und ROI

Kostenvergleich: Lokale Installation vs. HolySheep AI Cloud
KategorieLokale Tardis MachineHolySheep AIErsparnis
Server-Kosten/Monat€15-80 (VPS/Cloud)Ab $0 (Free Credits)90%+
API-NutzungEigene Keys nötigInklusive Credits100%
Setup-Zeit4-8 Stunden5 Minuten90%
WartungManuell + UpdatesVollständig verwaltet100%
Latenz1-5ms (lokal)< 50msGering
Verfügbarkeit取决于您的服务器99.9% SLAZuverlässiger

ROI-Analyse: Bei durchschnittlich 100.000 API-Requests/Monat sparen Sie mit HolySheep AI:

Warum HolySheep wählen

Als langjähriger Entwickler habe ich unzählige Stunden mit dem Aufsetzen eigener Infrastruktur verbracht. Der Schwenk zu HolySheep AI war eine der besten Entscheidungen für meine Projekte:

Meine Praxiserfahrung

Ich erinnere mich noch gut an mein erstes großes Projekt: Wir bauten einen AI-Chatbot für einen Kunden und mussten 50.000 Testanfragen durchführen. Mit meinem eigenen API-Key kostete das schnell über $200. Dann entdeckte ich HolySheep AI und war skeptisch – "zu gut, um wahr zu sein", dachte ich.

Nach der Registrierung (Registration in unter 2 Minuten) lud ich meine $10 auf und begann zu testen. Die Latenz war tatsächlich unter 50ms, die API-Kompatibilität perfekt. Innerhalb einer Woche hatten wir unsere gesamte Testsuite umgestellt.

Das Beste: Mit den Ersparnissen konnte ich das Budget für weitere Features nutzen statt für API-Kosten. Mittlerweile empfehle ich HolySheep allen Kollegen und Kunden – die Kombination aus Preis, Geschwindigkeit und Zuverlässigkeit ist unerreicht.

Fazit und Empfehlung

Die Tardis Machine ist ein mächtiges Werkzeug für lokale Entwicklung und Testing. Für Einzellösungen und kleine Teams ist die lokale Installation völlig ausreichend. Sobald Sie jedoch skalieren oder Zeit sparen möchten, ist HolySheep AI die clevere Wahl.

Mit Preisen ab $0.42/MTok für DeepSeek V3.2, weniger als 50ms Latenz und kostenlosen Startcredits gibt es kaum einen Grund, mehr zu bezahlen. Die Integration ist denkbar einfach:

// Mit HolySheep AI - direkt einsatzbereit
const response = await fetch('https://api.holysheep.ai/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_HOLYSHEEP_API_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    model: 'gpt-4.1',
    messages: [{ role: 'user', content: 'Erstelle einen tollen Artikel!' }]
  })
});

const data = await response.json();
console.log(data.choices[0].message.content);

Keine eigene Infrastruktur, keine Wartung, keine Überraschungen in der Rechnung. Probieren Sie es aus – mit den kostenlosen Credits können Sie risikofrei testen.

Starten Sie jetzt

Ob Sie sich für die lokale Tardis Machine oder HolySheep AI entscheiden – Sie haben jetzt alle Werkzeuge, um effizient mit AI-APIs zu arbeiten. Für die meisten Projekte empfehle ich HolySheep AI als Standardlösung wegen der unschlagbaren Kosten und des minimalen Setup-Aufwands.

👉 Registrieren Sie sich bei HolySheep AI — Startguthaben inklusive

Viel Erfolg bei Ihrem nächsten AI-Projekt! 🚀