SCCA logo
SCCA
SCCA logo|Documentation
overview

# Overview

SCCA (Secure Compact Chat Architecture) is an open-source protocol for building privacy-first AI chat applications. Every message is encrypted with AES-256-GCM using per-conversation keys derived via HKDF-SHA256. Conversations are stored as a single database row containing an encrypted token array, verified by a Merkle-HMAC integrity chain.

AES-256-GCM Encryption

Every message encrypted with unique per-conversation keys. Encryption happens before data reaches the database.

Single-Row Storage

~46 bytes overhead per message vs 200-300 bytes traditional. 1,000 messages in ~85 KB.

HKDF Key Hierarchy

Master Key → User Key → Conversation Key + Integrity Key. Keys never stored, always derived.

Merkle Integrity

HMAC-SHA256 chain across all tokens. Any modification invalidates the entire root.

> Design Principles

▸
Destructive EditingEditing message #5 in a 10-message conversation permanently deletes messages 6-10. No versioning, no branches. Linear timeline only.
▸
Minimal StorageCompact binary format with zlib compression. Every byte counts.
▸
Encryption at RestMessages are AES-256-GCM encrypted before storage. A database breach yields only encrypted blobs. (Not end-to-end: the server decrypts to build AI context.)
▸
Linear TimelineNo branches, no edit history. Only the current state exists. Radical simplicity.
▸
Real-Time SyncMultiple clients see updates simultaneously via SSE streaming.
quickstart

# Quick Start

Get SCCA running locally in minutes.

> 1. Clone and Install

bash
git clone https://github.com/Vii-Hunnid/SCCA.git
cd SCCA
npm install

> 2. Environment Variables

env
# .env
DATABASE_URL="postgresql://user:pass@host:5432/scca"
DIRECT_URL="postgresql://user:pass@host:5432/scca"
MASTER_KEY_SECRET="your-32-byte-hex-secret"
NEXTAUTH_SECRET="your-nextauth-secret"
NEXTAUTH_URL="http://localhost:3000"
GROQ_API_KEY="your-groq-api-key"

# Optional: OAuth providers
GITHUB_CLIENT_ID="..."
GITHUB_CLIENT_SECRET="..."
GOOGLE_CLIENT_ID="..."
GOOGLE_CLIENT_SECRET="..."

> 3. Database Setup

bash
npx prisma generate
npx prisma db push

> 4. Run

bash
npm run dev

Open http://localhost:3000 and register an account to start.

sdk

# SDK Setup

Integrate SCCA into your applications with our official SDKs and client libraries. Whether you're building a web app with Next.js or Nuxt.js, or mobile apps with React Native or Flutter, we've got you covered.

> Next.js (App Router)

For Next.js applications, create a reusable SCCA client that handles authentication and API calls. Install dependencies and set up the client:

bash
# Install dependencies
npm install @tanstack/react-query # Optional: for data fetching
typescript
// lib/scca-client.ts
const SCCA_BASE_URL = process.env.NEXT_PUBLIC_SCCA_API_URL || 
  "https://your-scca-instance.com";

export interface SCCAConfig {
  baseUrl: string;
  apiKey?: string; // For Vault API access
}

export class SCCAClient {
  private baseUrl: string;
  private apiKey?: string;

  constructor(config: SCCAConfig) {
    this.baseUrl = config.baseUrl;
    this.apiKey = config.apiKey;
  }

  // Vault API: Encrypt data
  async encrypt(data: string | string[], context: string) {
    const res = await fetch(`${this.baseUrl}/api/scca/vault/encrypt`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        ...(this.apiKey && { Authorization: `Bearer ${this.apiKey}` }),
      },
      credentials: "include",
      body: JSON.stringify({ data, context }),
    });
    if (!res.ok) throw new Error(`Encrypt failed: ${res.status}`);
    return res.json();
  }

  // Vault API: Decrypt data
  async decrypt(tokens: string[], context: string) {
    const res = await fetch(`${this.baseUrl}/api/scca/vault/decrypt`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        ...(this.apiKey && { Authorization: `Bearer ${this.apiKey}` }),
      },
      credentials: "include",
      body: JSON.stringify({ tokens, context }),
    });
    if (!res.ok) throw new Error(`Decrypt failed: ${res.status}`);
    return res.json();
  }

  // Chat: Create conversation
  async createConversation(title?: string) {
    const res = await fetch(`${this.baseUrl}/api/scca/conversations`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      credentials: "include",
      body: JSON.stringify({ title: title || "New Chat" }),
    });
    if (!res.ok) throw new Error(`Create conversation failed: ${res.status}`);
    return res.json();
  }

  // Chat: Send message with streaming
  async *sendMessageStream(
    conversationId: string,
    content: string,
    options?: { temperature?: number; max_tokens?: number }
  ): AsyncGenerator<{ token?: string; done?: boolean; error?: string }> {
    const res = await fetch(
      `${this.baseUrl}/api/scca/conversations/${conversationId}/messages`,
      {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        credentials: "include",
        body: JSON.stringify({
          content,
          temperature: options?.temperature ?? 0.7,
          max_tokens: options?.max_tokens ?? 4096,
        }),
      }
    );

    if (!res.ok) {
      yield { error: `HTTP ${res.status}` };
      return;
    }

    const reader = res.body!.getReader();
    const decoder = new TextDecoder();
    let buffer = "";

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;

      buffer += decoder.decode(value, { stream: true });
      const lines = buffer.split("\n");
      buffer = lines.pop() || "";

      for (const line of lines) {
        if (!line.startsWith("data: ")) continue;
        try {
          const data = JSON.parse(line.slice(6));
          yield data;
        } catch {
          // Ignore parse errors
        }
      }
    }
  }
}

// Singleton instance
export const scca = new SCCAClient({
  baseUrl: SCCA_BASE_URL,
});
typescript
// app/chat/page.tsx - Example usage in a Next.js page
"use client";

import { useState } from "react";
import { scca } from "@/lib/scca-client";

export default function ChatPage() {
  const [messages, setMessages] = useState<string[]>([]);
  const [input, setInput] = useState("");
  const [streaming, setStreaming] = useState(false);

  async function handleSend() {
    if (!input.trim()) return;

    // Add user message
    setMessages((prev) => [...prev, `You: ${input}`]);
    setInput("");
    setStreaming(true);

    try {
      // Create conversation (or use existing)
      const conv = await scca.createConversation("My Chat");

      // Stream AI response
      let response = "";
      for await (const event of scca.sendMessageStream(conv.id, input)) {
        if (event.token) {
          response += event.token;
          // Update UI with streaming content
          setMessages((prev) => [
            ...prev.slice(0, -1),
            `AI: ${response}`,
          ]);
        }
        if (event.done) {
          setStreaming(false);
        }
      }
    } catch (err) {
      console.error("Chat error:", err);
      setStreaming(false);
    }
  }

  return (
    <div className="p-4">
      <div className="space-y-2 mb-4">
        {messages.map((msg, i) => (
          <div key={i} className="p-2 bg-gray-100 rounded">{msg}</div>
        ))}
      </div>
      <div className="flex gap-2">
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyDown={(e) => e.key === "Enter" && handleSend()}
          className="flex-1 p-2 border rounded"
          placeholder="Type a message..."
          disabled={streaming}
        />
        <button
          onClick={handleSend}
          disabled={streaming}
          className="px-4 py-2 bg-blue-500 text-white rounded"
        >
          {streaming ? "..." : "Send"}
        </button>
      </div>
    </div>
  );
}

> Nuxt.js 3

For Nuxt.js applications, create a composable that handles SCCA integration:

bash
# Install dependencies
npm install @vueuse/core # Optional: for utilities
typescript
// composables/useSCCA.ts
const SCCA_BASE_URL = useRuntimeConfig().public.sccaApiUrl || 
  "https://your-scca-instance.com";

export interface SCCAClient {
  baseUrl: string;
  apiKey?: string;
}

export function useSCCA() {
  const config = useRuntimeConfig();
  const baseUrl = config.public.sccaApiUrl || "https://your-scca-instance.com";

  // Vault API: Encrypt data
  async function encrypt(data: string | string[], context: string) {
    const res = await $fetch(`/api/scca/vault/encrypt`, {
      baseURL: baseUrl,
      method: "POST",
      body: { data, context },
      credentials: "include",
    });
    return res;
  }

  // Vault API: Decrypt data
  async function decrypt(tokens: string[], context: string) {
    const res = await $fetch(`/api/scca/vault/decrypt`, {
      baseURL: baseUrl,
      method: "POST",
      body: { tokens, context },
      credentials: "include",
    });
    return res;
  }

  // Chat: Create conversation
  async function createConversation(title?: string) {
    const res = await $fetch(`/api/scca/conversations`, {
      baseURL: baseUrl,
      method: "POST",
      body: { title: title || "New Chat" },
      credentials: "include",
    });
    return res as { id: string; title: string; messageCount: number };
  }

  // Chat: Send message with streaming
  async function* sendMessageStream(
    conversationId: string,
    content: string,
    options?: { temperature?: number; max_tokens?: number }
  ): AsyncGenerator<{ token?: string; done?: boolean; error?: string }> {
    const res = await fetch(
      `${baseUrl}/api/scca/conversations/${conversationId}/messages`,
      {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        credentials: "include",
        body: JSON.stringify({
          content,
          temperature: options?.temperature ?? 0.7,
          max_tokens: options?.max_tokens ?? 4096,
        }),
      }
    );

    if (!res.ok) {
      yield { error: `HTTP ${res.status}` };
      return;
    }

    const reader = res.body!.getReader();
    const decoder = new TextDecoder();
    let buffer = "";

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;

      buffer += decoder.decode(value, { stream: true });
      const lines = buffer.split("\n");
      buffer = lines.pop() || "";

      for (const line of lines) {
        if (!line.startsWith("data: ")) continue;
        try {
          const data = JSON.parse(line.slice(6));
          yield data;
        } catch {
          // Ignore parse errors
        }
      }
    }
  }

  return {
    encrypt,
    decrypt,
    createConversation,
    sendMessageStream,
  };
}
vue
<!-- pages/chat.vue - Example Nuxt page -->
<template>
  <div class="p-4 max-w-2xl mx-auto">
    <h1 class="text-2xl font-bold mb-4">SCCA Chat</h1>
    
    <div class="space-y-2 mb-4 h-96 overflow-y-auto border rounded p-4">
      <div
        v-for="(msg, i) in messages"
        :key="i"
        :class="[
          'p-2 rounded',
          msg.role === 'user' ? 'bg-blue-100 ml-auto max-w-[80%]' : 'bg-gray-100 mr-auto max-w-[80%]'
        ]"
      >
        {{ msg.content }}
      </div>
      <div v-if="streaming" class="text-gray-400">AI is typing...</div>
    </div>

    <div class="flex gap-2">
      <input
        v-model="input"
        @keydown.enter="handleSend"
        :disabled="streaming"
        class="flex-1 p-2 border rounded"
        placeholder="Type a message..."
      />
      <button
        @click="handleSend"
        :disabled="streaming || !input.trim()"
        class="px-4 py-2 bg-blue-500 text-white rounded disabled:opacity-50"
      >
        {{ streaming ? '...' : 'Send' }}
      </button>
    </div>
  </div>
</template>

<script setup lang="ts">
const { createConversation, sendMessageStream } = useSCCA();

const messages = ref<Array<{ role: 'user' | 'assistant'; content: string }>>([]);
const input = ref('');
const streaming = ref(false);
const conversationId = ref<string | null>(null);

async function handleSend() {
  if (!input.value.trim()) return;

  // Add user message
  messages.value.push({ role: 'user', content: input.value });
  const userMessage = input.value;
  input.value = '';
  streaming.value = true;

  try {
    // Create conversation if needed
    if (!conversationId.value) {
      const conv = await createConversation('My Chat');
      conversationId.value = conv.id;
    }

    // Stream AI response
    let response = '';
    for await (const event of sendMessageStream(conversationId.value, userMessage)) {
      if (event.token) {
        response += event.token;
        // Update last message or add new one
        const lastMsg = messages.value[messages.value.length - 1];
        if (lastMsg.role === 'assistant') {
          lastMsg.content = response;
        } else {
          messages.value.push({ role: 'assistant', content: response });
        }
      }
      if (event.done) {
        streaming.value = false;
      }
    }
  } catch (err) {
    console.error('Chat error:', err);
    streaming.value = false;
  }
}
</script>

> React Native

For mobile apps with React Native, use the Fetch API with AsyncStorage for session management:

bash
# Install dependencies
npm install @react-native-async-storage/async-storage
typescript
// lib/scca-native.ts
import AsyncStorage from '@react-native-async-storage/async-storage';

const SCCA_BASE_URL = 'https://your-scca-instance.com';

export class SCCANativeClient {
  private baseUrl: string;

  constructor(baseUrl: string = SCCA_BASE_URL) {
    this.baseUrl = baseUrl;
  }

  // Store session cookie after login
  async setSessionCookie(cookie: string) {
    await AsyncStorage.setItem('@scca_session', cookie);
  }

  // Get stored session
  async getSessionCookie(): Promise<string | null> {
    return await AsyncStorage.getItem('@scca_session');
  }

  // Clear session on logout
  async clearSession() {
    await AsyncStorage.removeItem('@scca_session');
  }

  // Authenticate user
  async login(email: string, password: string): Promise<boolean> {
    try {
      // Get CSRF token
      const csrfRes = await fetch(`${this.baseUrl}/api/auth/csrf`);
      const { csrfToken } = await csrfRes.json();

      // Extract cookie from response
      const setCookie = csrfRes.headers.get('set-cookie');

      // Sign in
      const loginRes = await fetch(
        `${this.baseUrl}/api/auth/callback/credentials`,
        {
          method: 'POST',
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            Cookie: setCookie || '',
          },
          body: `email=${encodeURIComponent(email)}&password=${encodeURIComponent(
            password
          )}&csrfToken=${csrfToken}`,
        }
      );

      if (loginRes.ok) {
        const sessionCookie = loginRes.headers.get('set-cookie');
        if (sessionCookie) {
          await this.setSessionCookie(sessionCookie);
          return true;
        }
      }
      return false;
    } catch (err) {
      console.error('Login error:', err);
      return false;
    }
  }

  // Make authenticated request
  private async authenticatedFetch(
    url: string,
    options: RequestInit = {}
  ): Promise<Response> {
    const sessionCookie = await this.getSessionCookie();
    return fetch(url, {
      ...options,
      headers: {
        ...options.headers,
        ...(sessionCookie && { Cookie: sessionCookie }),
      },
    });
  }

  // Vault API: Encrypt data
  async encrypt(data: string | string[], context: string) {
    const res = await this.authenticatedFetch(
      `${this.baseUrl}/api/scca/vault/encrypt`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ data, context }),
      }
    );
    if (!res.ok) throw new Error(`Encrypt failed: ${res.status}`);
    return res.json();
  }

  // Chat: Create conversation
  async createConversation(title?: string) {
    const res = await this.authenticatedFetch(
      `${this.baseUrl}/api/scca/conversations`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ title: title || 'New Chat' }),
      }
    );
    if (!res.ok) throw new Error(`Create failed: ${res.status}`);
    return res.json();
  }

  // Chat: Send message (returns async iterator for streaming)
  async *sendMessageStream(
    conversationId: string,
    content: string
  ): AsyncGenerator<{ token?: string; done?: boolean }> {
    const res = await this.authenticatedFetch(
      `${this.baseUrl}/api/scca/conversations/${conversationId}/messages`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ content, temperature: 0.7 }),
      }
    );

    if (!res.ok) throw new Error(`Send failed: ${res.status}`);

    const reader = res.body!.getReader();
    const decoder = new TextDecoder();
    let buffer = '';

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;

      buffer += decoder.decode(value, { stream: true });
      const lines = buffer.split('\n');
      buffer = lines.pop() || '';

      for (const line of lines) {
        if (!line.startsWith('data: ')) continue;
        try {
          const data = JSON.parse(line.slice(6));
          yield data;
        } catch {
          // Ignore parse errors
        }
      }
    }
  }
}

export const sccaNative = new SCCANativeClient();
typescript
// components/ChatScreen.tsx
import React, { useState, useCallback } from 'react';
import {
  View,
  Text,
  TextInput,
  FlatList,
  TouchableOpacity,
  StyleSheet,
} from 'react-native';
import { sccaNative } from '../lib/scca-native';

interface Message {
  id: string;
  role: 'user' | 'assistant';
  content: string;
}

export function ChatScreen() {
  const [messages, setMessages] = useState<Message[]>([]);
  const [input, setInput] = useState('');
  const [streaming, setStreaming] = useState(false);
  const [conversationId, setConversationId] = useState<string | null>(null);

  const sendMessage = useCallback(async () => {
    if (!input.trim() || streaming) return;

    const userMsg: Message = {
      id: Date.now().toString(),
      role: 'user',
      content: input,
    };
    setMessages((prev) => [...prev, userMsg]);
    setInput('');
    setStreaming(true);

    try {
      // Create conversation if needed
      let convId = conversationId;
      if (!convId) {
        const conv = await sccaNative.createConversation('Mobile Chat');
        convId = conv.id;
        setConversationId(convId);
      }

      // Add placeholder for AI response
      const aiMsgId = (Date.now() + 1).toString();
      setMessages((prev) => [
        ...prev,
        { id: aiMsgId, role: 'assistant', content: '' },
      ]);

      // Stream response
      for await (const event of sccaNative.sendMessageStream(convId, userMsg.content)) {
        if (event.token) {
          setMessages((prev) =>
            prev.map((msg) =>
              msg.id === aiMsgId
                ? { ...msg, content: msg.content + event.token }
                : msg
            )
          );
        }
        if (event.done) {
          setStreaming(false);
        }
      }
    } catch (err) {
      console.error('Chat error:', err);
      setStreaming(false);
    }
  }, [input, streaming, conversationId]);

  return (
    <View style={styles.container}>
      <FlatList
        data={messages}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <View
            style={[
              styles.message,
              item.role === 'user' ? styles.userMsg : styles.aiMsg,
            ]}
          >
            <Text>{item.content}</Text>
          </View>
        )}
      />
      <View style={styles.inputContainer}>
        <TextInput
          style={styles.input}
          value={input}
          onChangeText={setInput}
          placeholder="Type a message..."
          onSubmitEditing={sendMessage}
          editable={!streaming}
        />
        <TouchableOpacity
          onPress={sendMessage}
          disabled={streaming || !input.trim()}
          style={[styles.sendBtn, streaming && styles.disabled]}
        >
          <Text style={styles.sendText}>Send</Text>
        </TouchableOpacity>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 16 },
  message: { padding: 12, marginVertical: 4, borderRadius: 8 },
  userMsg: { backgroundColor: '#e3f2fd', alignSelf: 'flex-end' },
  aiMsg: { backgroundColor: '#f5f5f5', alignSelf: 'flex-start' },
  inputContainer: { flexDirection: 'row', gap: 8, paddingTop: 8 },
  input: { flex: 1, borderWidth: 1, padding: 12, borderRadius: 8 },
  sendBtn: { backgroundColor: '#2196f3', padding: 12, borderRadius: 8 },
  sendText: { color: 'white' },
  disabled: { opacity: 0.5 },
});

> Flutter

For Flutter apps, use the http package with shared_preferences for session storage:

yaml
# pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  http: ^1.2.0
  shared_preferences: ^2.2.2
  uuid: ^4.3.3
dart
// lib/services/scca_service.dart
import 'dart:convert';
import 'dart:async';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';

class SCCAService {
  final String baseUrl;
  late final SharedPreferences _prefs;

  SCCAService({required this.baseUrl});

  Future<void> init() async {
    _prefs = await SharedPreferences.getInstance();
  }

  // Session management
  Future<void> setSessionCookie(String cookie) async {
    await _prefs.setString('scca_session', cookie);
  }

  String? getSessionCookie() {
    return _prefs.getString('scca_session');
  }

  Future<void> clearSession() async {
    await _prefs.remove('scca_session');
  }

  // Authenticated HTTP request
  Future<http.Response> _authenticatedRequest(
    String method,
    String path, {
    Map<String, dynamic>? body,
  }) async {
    final url = Uri.parse('$baseUrl$path');
    final headers = {
      'Content-Type': 'application/json',
      if (getSessionCookie() != null) 'Cookie': getSessionCookie()!,
    };

    switch (method) {
      case 'GET':
        return await http.get(url, headers: headers);
      case 'POST':
        return await http.post(url, headers: headers, body: jsonEncode(body));
      case 'DELETE':
        return await http.delete(url, headers: headers);
      default:
        throw Exception('Unsupported method: $method');
    }
  }

  // Login
  Future<bool> login(String email, String password) async {
    try {
      // Get CSRF token
      final csrfRes = await http.get(Uri.parse('$baseUrl/api/auth/csrf'));
      final csrfData = jsonDecode(csrfRes.body);
      final csrfToken = csrfData['csrfToken'];

      // Get cookies from response
      final setCookie = csrfRes.headers['set-cookie'];

      // Sign in
      final loginRes = await http.post(
        Uri.parse('$baseUrl/api/auth/callback/credentials'),
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
          if (setCookie != null) 'Cookie': setCookie,
        },
        body:
            'email=${Uri.encodeComponent(email)}&password=${Uri.encodeComponent(password)}&csrfToken=$csrfToken',
      );

      if (loginRes.statusCode == 200 || loginRes.statusCode == 302) {
        final sessionCookie = loginRes.headers['set-cookie'];
        if (sessionCookie != null) {
          await setSessionCookie(sessionCookie);
          return true;
        }
      }
      return false;
    } catch (e) {
      print('Login error: $e');
      return false;
    }
  }

  // Vault API: Encrypt
  Future<Map<String, dynamic>> encrypt(
    dynamic data,
    String context,
  ) async {
    final res = await _authenticatedRequest(
      'POST',
      '/api/scca/vault/encrypt',
      body: {'data': data, 'context': context},
    );
    if (res.statusCode != 200) throw Exception('Encrypt failed: ${res.statusCode}');
    return jsonDecode(res.body);
  }

  // Vault API: Decrypt
  Future<Map<String, dynamic>> decrypt(
    List<String> tokens,
    String context,
  ) async {
    final res = await _authenticatedRequest(
      'POST',
      '/api/scca/vault/decrypt',
      body: {'tokens': tokens, 'context': context},
    );
    if (res.statusCode != 200) throw Exception('Decrypt failed: ${res.statusCode}');
    return jsonDecode(res.body);
  }

  // Chat: Create conversation
  Future<Map<String, dynamic>> createConversation({String? title}) async {
    final res = await _authenticatedRequest(
      'POST',
      '/api/scca/conversations',
      body: {'title': title ?? 'New Chat'},
    );
    if (res.statusCode != 201) throw Exception('Create failed: ${res.statusCode}');
    return jsonDecode(res.body);
  }

  // Chat: Send message with streaming
  Stream<Map<String, dynamic>> sendMessageStream(
    String conversationId,
    String content,
  ) async* {
    final url = Uri.parse(
      '$baseUrl/api/scca/conversations/$conversationId/messages',
    );
    final request = http.Request('POST', url);
    request.headers['Content-Type'] = 'application/json';
    if (getSessionCookie() != null) {
      request.headers['Cookie'] = getSessionCookie()!;
    }
    request.body = jsonEncode({
      'content': content,
      'temperature': 0.7,
      'max_tokens': 4096,
    });

    final streamedRes = await request.send();

    await for (final chunk in streamedRes.stream.transform(utf8.decoder)) {
      final lines = chunk.split('\n');
      for (final line in lines) {
        if (line.startsWith('data: ')) {
          try {
            final data = jsonDecode(line.substring(6));
            yield data;
          } catch (e) {
            // Ignore parse errors
          }
        }
      }
    }
  }
}
dart
// lib/screens/chat_screen.dart
import 'package:flutter/material.dart';
import '../services/scca_service.dart';

class ChatScreen extends StatefulWidget {
  @override
  _ChatScreenState createState() => _ChatScreenState();
}

class _ChatScreenState extends State<ChatScreen> {
  final SCCAService _scca = SCCAService(baseUrl: 'https://your-scca-instance.com');
  final TextEditingController _controller = TextEditingController();
  final List<Map<String, dynamic>> _messages = [];
  String? _conversationId;
  bool _streaming = false;
  StreamSubscription? _streamSubscription;

  @override
  void initState() {
    super.initState();
    _scca.init();
  }

  Future<void> _sendMessage() async {
    final content = _controller.text.trim();
    if (content.isEmpty || _streaming) return;

    setState(() {
      _messages.add({'role': 'user', 'content': content});
      _controller.clear();
      _streaming = true;
    });

    try {
      // Create conversation if needed
      if (_conversationId == null) {
        final conv = await _scca.createConversation(title: 'Flutter Chat');
        _conversationId = conv['id'];
      }

      // Add placeholder for AI response
      setState(() {
        _messages.add({'role': 'assistant', 'content': ''});
      });

      // Stream response
      _streamSubscription = _scca
          .sendMessageStream(_conversationId!, content)
          .listen(
            (event) {
              if (event['token'] != null) {
                setState(() {
                  _messages.last['content'] += event['token'];
                });
              }
              if (event['done'] == true) {
                setState(() => _streaming = false);
              }
            },
            onError: (e) {
              print('Stream error: $e');
              setState(() => _streaming = false);
            },
          );
    } catch (e) {
      print('Send error: $e');
      setState(() => _streaming = false);
    }
  }

  @override
  void dispose() {
    _streamSubscription?.cancel();
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('SCCA Chat')),
      body: Column(
        children: [
          Expanded(
            child: ListView.builder(
              padding: EdgeInsets.all(16),
              itemCount: _messages.length,
              itemBuilder: (ctx, i) {
                final msg = _messages[i];
                final isUser = msg['role'] == 'user';
                return Align(
                  alignment: isUser ? Alignment.centerRight : Alignment.centerLeft,
                  child: Container(
                    margin: EdgeInsets.symmetric(vertical: 4),
                    padding: EdgeInsets.all(12),
                    decoration: BoxDecoration(
                      color: isUser ? Colors.blue[100] : Colors.grey[200],
                      borderRadius: BorderRadius.circular(12),
                    ),
                    constraints: BoxConstraints(
                      maxWidth: MediaQuery.of(context).size.width * 0.8,
                    ),
                    child: Text(msg['content']),
                  ),
                );
              },
            ),
          ),
          if (_streaming) LinearProgressIndicator(),
          Padding(
            padding: EdgeInsets.all(16),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _controller,
                    decoration: InputDecoration(
                      hintText: 'Type a message...',
                      border: OutlineInputBorder(),
                    ),
                    onSubmitted: (_) => _sendMessage(),
                    enabled: !_streaming,
                  ),
                ),
                SizedBox(width: 8),
                ElevatedButton(
                  onPressed: _streaming ? null : _sendMessage,
                  child: Text('Send'),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

> iOS (Swift)

For native iOS apps using Swift and URLSession:

swift
// SCCAClient.swift
import Foundation

class SCCAClient {
    let baseURL: URL
    private var sessionCookie: String?
    
    init(baseURL: String) {
        self.baseURL = URL(string: baseURL)!
    }
    
    // Store session in UserDefaults
    func setSessionCookie(_ cookie: String) {
        sessionCookie = cookie
        UserDefaults.standard.set(cookie, forKey: "scca_session")
    }
    
    func loadSessionCookie() {
        sessionCookie = UserDefaults.standard.string(forKey: "scca_session")
    }
    
    func clearSession() {
        sessionCookie = nil
        UserDefaults.standard.removeObject(forKey: "scca_session")
    }
    
    // MARK: - Authentication
    
    func login(email: String, password: String) async throws -> Bool {
        // Get CSRF token
        let csrfURL = baseURL.appendingPathComponent("/api/auth/csrf")
        let (csrfData, csrfResponse) = try await URLSession.shared.data(from: csrfURL)
        
        guard let csrfJson = try? JSONSerialization.jsonObject(with: csrfData) as? [String: Any],
              let csrfToken = csrfJson["csrfToken"] as? String else {
            return false
        }
        
        // Extract cookie from response
        if let headers = csrfResponse as? HTTPURLResponse,
           let setCookie = headers.allHeaderFields["Set-Cookie"] as? String {
            // Store initial cookie
        }
        
        // Sign in
        let loginURL = baseURL.appendingPathComponent("/api/auth/callback/credentials")
        var request = URLRequest(url: loginURL)
        request.httpMethod = "POST"
        request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
        
        let body = "email=\(email.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)&password=\(password.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)&csrfToken=\(csrfToken)"
        request.httpBody = body.data(using: .utf8)
        
        let (_, loginResponse) = try await URLSession.shared.data(for: request)
        
        guard let httpResponse = loginResponse as? HTTPURLResponse,
              (200...302).contains(httpResponse.statusCode),
              let sessionCookie = httpResponse.allHeaderFields["Set-Cookie"] as? String else {
            return false
        }
        
        setSessionCookie(sessionCookie)
        return true
    }
    
    // MARK: - Vault API
    
    func encrypt(data: [String], context: String) async throws -> [String: Any] {
        let url = baseURL.appendingPathComponent("/api/scca/vault/encrypt")
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        if let cookie = sessionCookie {
            request.setValue(cookie, forHTTPHeaderField: "Cookie")
        }
        
        let body: [String: Any] = ["data": data, "context": context]
        request.httpBody = try JSONSerialization.data(withJSONObject: body)
        
        let (data, _) = try await URLSession.shared.data(for: request)
        guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
            throw SCCAError.invalidResponse
        }
        return json
    }
    
    func decrypt(tokens: [String], context: String) async throws -> [String: Any] {
        let url = baseURL.appendingPathComponent("/api/scca/vault/decrypt")
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        if let cookie = sessionCookie {
            request.setValue(cookie, forHTTPHeaderField: "Cookie")
        }
        
        let body: [String: Any] = ["tokens": tokens, "context": context]
        request.httpBody = try JSONSerialization.data(withJSONObject: body)
        
        let (data, _) = try await URLSession.shared.data(for: request)
        guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
            throw SCCAError.invalidResponse
        }
        return json
    }
    
    // MARK: - Chat API
    
    func createConversation(title: String = "New Chat") async throws -> [String: Any] {
        let url = baseURL.appendingPathComponent("/api/scca/conversations")
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        if let cookie = sessionCookie {
            request.setValue(cookie, forHTTPHeaderField: "Cookie")
        }
        
        let body: [String: Any] = ["title": title]
        request.httpBody = try JSONSerialization.data(withJSONObject: body)
        
        let (data, _) = try await URLSession.shared.data(for: request)
        guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
            throw SCCAError.invalidResponse
        }
        return json
    }
    
    func sendMessageStream(
        conversationId: String,
        content: String,
        onToken: @escaping (String) -> Void,
        onComplete: @escaping ([String: Any]) -> Void,
        onError: @escaping (Error) -> Void
    ) {
        let url = baseURL.appendingPathComponent("/api/scca/conversations/\(conversationId)/messages")
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        if let cookie = sessionCookie {
            request.setValue(cookie, forHTTPHeaderField: "Cookie")
        }
        
        let body: [String: Any] = [
            "content": content,
            "temperature": 0.7,
            "max_tokens": 4096
        ]
        request.httpBody = try? JSONSerialization.data(withJSONObject: body)
        
        let task = URLSession.shared.dataTask(with: request) { data, response, error in
            if let error = error {
                onError(error)
                return
            }
            
            guard let data = data else {
                onError(SCCAError.noData)
                return
            }
            
            // Parse SSE stream
            let text = String(data: data, encoding: .utf8) ?? ""
            let lines = text.components(separatedBy: "\n")
            
            for line in lines {
                if line.hasPrefix("data: ") {
                    let jsonStr = String(line.dropFirst(6))
                    if let jsonData = jsonStr.data(using: .utf8),
                       let json = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any] {
                        if let token = json["token"] as? String {
                            DispatchQueue.main.async {
                                onToken(token)
                            }
                        } else if json["done"] as? Bool == true {
                            DispatchQueue.main.async {
                                onComplete(json)
                            }
                        }
                    }
                }
            }
        }
        
        task.resume()
    }
}

enum SCCAError: Error {
    case invalidResponse
    case noData
    case authenticationFailed
}

> Android (Kotlin)

For native Android apps using Kotlin and OkHttp:

kotlin
// build.gradle.kts
dependencies {
    implementation("com.squareup.okhttp3:okhttp:4.12.0")
    implementation("com.squareup.okhttp3:okhttp-eventsource:4.12.0")
    implementation("org.json:json:20231013")
}
kotlin
// SCCAClient.kt
package com.example.scca

import android.content.Context
import android.content.SharedPreferences
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import okhttp3.*
import okhttp3.EventSource
import okhttp3.EventSourceListener
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONArray
import org.json.JSONObject
import java.io.IOException

class SCCAClient(
    private val context: Context,
    private val baseUrl: String
) {
    private val client = OkHttpClient()
    private val prefs: SharedPreferences = 
        context.getSharedPreferences("scca", Context.MODE_PRIVATE)
    
    companion object {
        private const val SESSION_COOKIE_KEY = "scca_session"
        private val JSON = "application/json; charset=utf-8".toMediaType()
    }
    
    // Session management
    fun setSessionCookie(cookie: String) {
        prefs.edit().putString(SESSION_COOKIE_KEY, cookie).apply()
    }
    
    fun getSessionCookie(): String? {
        return prefs.getString(SESSION_COOKIE_KEY, null)
    }
    
    fun clearSession() {
        prefs.edit().remove(SESSION_COOKIE_KEY).apply()
    }
    
    // Authentication
    suspend fun login(email: String, password: String): Boolean {
        return try {
            // Get CSRF token
            val csrfRequest = Request.Builder()
                .url("$baseUrl/api/auth/csrf")
                .build()
            
            val csrfResponse = client.newCall(csrfRequest).execute()
            val csrfJson = JSONObject(csrfResponse.body?.string() ?: "{}")
            val csrfToken = csrfJson.getString("csrfToken")
            val initialCookie = csrfResponse.headers("Set-Cookie").firstOrNull()
            
            // Sign in
            val formBody = FormBody.Builder()
                .add("email", email)
                .add("password", password)
                .add("csrfToken", csrfToken)
                .build()
            
            val loginRequest = Request.Builder()
                .url("$baseUrl/api/auth/callback/credentials")
                .post(formBody)
                .apply { 
                    initialCookie?.let { addHeader("Cookie", it) }
                }
                .build()
            
            val loginResponse = client.newCall(loginRequest).execute()
            
            if (loginResponse.isSuccessful || loginResponse.code == 302) {
                val sessionCookie = loginResponse.headers("Set-Cookie").firstOrNull()
                sessionCookie?.let { setSessionCookie(it) }
                sessionCookie != null
            } else {
                false
            }
        } catch (e: Exception) {
            e.printStackTrace()
            false
        }
    }
    
    // Vault API: Encrypt
    fun encrypt(data: List<String>, context: String): Result<JSONObject> {
        return try {
            val body = JSONObject().apply {
                put("data", JSONArray(data))
                put("context", context)
            }
            
            val request = Request.Builder()
                .url("$baseUrl/api/scca/vault/encrypt")
                .post(body.toString().toRequestBody(JSON))
                .addHeader("Content-Type", "application/json")
                .apply { getSessionCookie()?.let { addHeader("Cookie", it) } }
                .build()
            
            val response = client.newCall(request).execute()
            
            if (response.isSuccessful) {
                Result.success(JSONObject(response.body?.string() ?: "{}"))
            } else {
                Result.failure(IOException("Encrypt failed: ${response.code}"))
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
    
    // Vault API: Decrypt
    fun decrypt(tokens: List<String>, context: String): Result<JSONObject> {
        return try {
            val body = JSONObject().apply {
                put("tokens", JSONArray(tokens))
                put("context", context)
            }
            
            val request = Request.Builder()
                .url("$baseUrl/api/scca/vault/decrypt")
                .post(body.toString().toRequestBody(JSON))
                .addHeader("Content-Type", "application/json")
                .apply { getSessionCookie()?.let { addHeader("Cookie", it) } }
                .build()
            
            val response = client.newCall(request).execute()
            
            if (response.isSuccessful) {
                Result.success(JSONObject(response.body?.string() ?: "{}"))
            } else {
                Result.failure(IOException("Decrypt failed: ${response.code}"))
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
    
    // Chat: Create conversation
    fun createConversation(title: String = "New Chat"): Result<JSONObject> {
        return try {
            val body = JSONObject().put("title", title)
            
            val request = Request.Builder()
                .url("$baseUrl/api/scca/conversations")
                .post(body.toString().toRequestBody(JSON))
                .addHeader("Content-Type", "application/json")
                .apply { getSessionCookie()?.let { addHeader("Cookie", it) } }
                .build()
            
            val response = client.newCall(request).execute()
            
            if (response.isSuccessful) {
                Result.success(JSONObject(response.body?.string() ?: "{}"))
            } else {
                Result.failure(IOException("Create failed: ${response.code}"))
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
    
    // Chat: Send message with streaming
    fun sendMessageStream(conversationId: String, content: String): Flow<SSEEvent> = flow {
        val body = JSONObject().apply {
            put("content", content)
            put("temperature", 0.7)
            put("max_tokens", 4096)
        }
        
        val request = Request.Builder()
            .url("$baseUrl/api/scca/conversations/$conversationId/messages")
            .post(body.toString().toRequestBody(JSON))
            .addHeader("Content-Type", "application/json")
            .apply { getSessionCookie()?.let { addHeader("Cookie", it) } }
            .build()
        
        val response = client.newCall(request).execute()
        
        if (!response.isSuccessful) {
            emit(SSEEvent.Error("HTTP ${response.code}"))
            return@flow
        }
        
        val source = response.body?.source()
        val buffer = okio.Buffer()
        
        while (!source!!.exhausted()) {
            source.read(buffer, 8192)
            val chunk = buffer.readUtf8()
            val lines = chunk.split("\n")
            
            for (line in lines) {
                if (line.startsWith("data: ")) {
                    val jsonStr = line.substring(6)
                    try {
                        val json = JSONObject(jsonStr)
                        when {
                            json.has("token") -> emit(SSEEvent.Token(json.getString("token")))
                            json.optBoolean("done") -> emit(SSEEvent.Done(json))
                            json.has("error") -> emit(SSEEvent.Error(json.getString("error")))
                        }
                    } catch (e: Exception) {
                        // Ignore parse errors
                    }
                }
            }
        }
    }.flowOn(Dispatchers.IO)
}

sealed class SSEEvent {
    data class Token(val content: String) : SSEEvent()
    data class Done(val data: JSONObject) : SSEEvent()
    data class Error(val message: String) : SSEEvent()
}
⚙
API Key Authentication

For Vault API access (encrypt/decrypt), you can use API key authentication instead of session cookies. Generate an API key from your Dashboard, then pass it in theAuthorization: Bearer scca_k_... header. This is recommended for backend services and mobile apps that don't need full session management.

vault

# Vault API

Use SCCA's encryption engine directly — encrypt, decrypt, and verify any data through the Vault API. No chat required. Store the encrypted tokens in your own database, pass them between services, or use them anywhere you need AES-256-GCM + zlib compression with Merkle integrity.

> Authentication with API Keys

Generate an API key from your Dashboard > API Keys page. Use it in the Authorization header for all Vault API requests.

bash
# All Vault API requests use Bearer auth:
curl -X POST https://your-scca-instance.com/api/scca/vault/encrypt \
  -H "Authorization: Bearer scca_k_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"data": "hello world", "context": "my-project"}'
EndpointDescription
POST /api/scca/keysGenerate a new API key (session auth only)
GET /api/scca/keysList your active keys (session auth only)
DELETE /api/scca/keys/[id]Revoke an API key (session auth only)
json
// POST /api/scca/keys — Generate a new key
// Request (requires session cookie auth)
{ "name": "Production Backend", "expiresInDays": 90 }

// Response (key shown ONCE, save it immediately)
{
  "id": "clx...",
  "name": "Production Backend",
  "key": "scca_k_a1b2c3d4e5f6...",
  "keyPrefix": "scca_k_a1b2...",
  "expiresAt": "2026-05-10T00:00:00.000Z",
  "warning": "Save this key now. It will not be shown again."
}
▸
How it works

Every authenticated user gets their own derived encryption keys. The context parameter isolates keys per project/use case — data encrypted under "billing" cannot be decrypted with "user-data", even by the same user. The server handles all crypto. You send plaintext in, get encrypted tokens out.

> Encrypt Data

POST/api/scca/vault/encrypt

Encrypt one or more strings. Returns encrypted tokens and a Merkle integrity root.

json
// Request
{
  "data": "sensitive user data to encrypt",
  "context": "my-app-billing"
}

// Or encrypt multiple items at once
{
  "data": [
    "first item to encrypt",
    "second item to encrypt",
    "third item to encrypt"
  ],
  "context": "my-app-billing"
}

// Response: 200 OK
{
  "tokens": [
    "AQAAAAAn..."
  ],
  "merkleRoot": "a1b2c3d4e5f6...",
  "context": "my-app-billing",
  "metadata": {
    "itemCount": 1,
    "originalBytes": 30,
    "encryptedBytes": 1098,
    "compressionRatio": 0.365,
    "cipher": "AES-256-GCM",
    "kdf": "HKDF-SHA256",
    "integrity": "HMAC-SHA256-chain"
  }
}

> Decrypt Data

POST/api/scca/vault/decrypt

Decrypt tokens back to plaintext. Must use the same context as encryption.

json
// Request
{
  "tokens": ["AQAAAAAn..."],
  "context": "my-app-billing"
}

// Response: 200 OK
{
  "data": [
    {
      "content": "sensitive user data to encrypt",
      "sequence": 0,
      "timestamp": "2026-02-09T12:00:00.000Z",
      "contentHash": "a1b2c3d4e5f67890"
    }
  ],
  "context": "my-app-billing"
}

> Verify Integrity

POST/api/scca/vault/verify

Verify that tokens haven't been tampered with using the Merkle-HMAC chain.

json
// Request
{
  "tokens": ["AQAAAAAn...", "AQEAAAAn..."],
  "merkleRoot": "a1b2c3d4e5f6...",
  "context": "my-app-billing"
}

// Response: 200 OK
{
  "valid": true,
  "merkleRootMatch": true,
  "computedRoot": "a1b2c3d4e5f6...",
  "tokenCount": 2,
  "errors": [],
  "lastValidSequence": 1
}

> Usage: Encrypt Data in Your System (cURL)

bash
# Authenticate first (see Authentication section below)

# Encrypt sensitive data
curl -s -b cookies.txt \
  -X POST https://your-scca-instance.com/api/scca/vault/encrypt \
  -H "Content-Type: application/json" \
  -d '{
    "data": ["user SSN: 123-45-6789", "credit card: 4111-1111-1111-1111"],
    "context": "pii-vault"
  }' | jq .

# Store the tokens and merkleRoot in your own database
# Later, decrypt when needed:
curl -s -b cookies.txt \
  -X POST https://your-scca-instance.com/api/scca/vault/decrypt \
  -H "Content-Type: application/json" \
  -d '{
    "tokens": ["<token-from-encrypt>", "<token-from-encrypt>"],
    "context": "pii-vault"
  }' | jq .data

# Verify nothing was tampered with:
curl -s -b cookies.txt \
  -X POST https://your-scca-instance.com/api/scca/vault/verify \
  -H "Content-Type: application/json" \
  -d '{
    "tokens": ["<token-from-encrypt>", "<token-from-encrypt>"],
    "merkleRoot": "<root-from-encrypt>",
    "context": "pii-vault"
  }' | jq .valid

> Usage: JavaScript / TypeScript

typescript
const SCCA = "https://your-scca-instance.com";

// Encrypt data for storage
async function encryptData(data: string | string[], context: string) {
  const res = await fetch(`${SCCA}/api/scca/vault/encrypt`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    credentials: "include",
    body: JSON.stringify({ data, context }),
  });
  return res.json();
  // { tokens: [...], merkleRoot: "...", metadata: {...} }
}

// Decrypt tokens back to plaintext
async function decryptData(tokens: string[], context: string) {
  const res = await fetch(`${SCCA}/api/scca/vault/decrypt`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    credentials: "include",
    body: JSON.stringify({ tokens, context }),
  });
  return res.json();
  // { data: [{ content, sequence, timestamp, contentHash }] }
}

// Verify integrity
async function verifyData(
  tokens: string[],
  merkleRoot: string,
  context: string
) {
  const res = await fetch(`${SCCA}/api/scca/vault/verify`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    credentials: "include",
    body: JSON.stringify({ tokens, merkleRoot, context }),
  });
  return res.json();
  // { valid: true/false, errors: [...] }
}

// ── Example: Encrypt user PII before storing in your DB ──

async function storeUserData(userId: string, sensitiveFields: string[]) {
  // Encrypt with SCCA — each user gets a unique context
  const result = await encryptData(sensitiveFields, `user-${userId}-pii`);

  // Store encrypted tokens in YOUR database
  await yourDB.users.update({
    where: { id: userId },
    data: {
      encryptedPII: result.tokens,       // string[]
      piiMerkleRoot: result.merkleRoot,  // for verification
    },
  });
}

async function readUserData(userId: string) {
  const user = await yourDB.users.findUnique({ where: { id: userId } });

  // Verify integrity first
  const check = await verifyData(
    user.encryptedPII,
    user.piiMerkleRoot,
    `user-${userId}-pii`
  );
  if (!check.valid) throw new Error("Data tampered with!");

  // Decrypt
  const result = await decryptData(
    user.encryptedPII,
    `user-${userId}-pii`
  );
  return result.data.map((d: any) => d.content);
}

> Usage: Python

python
import requests, json

class SCCAVault:
    def __init__(self, base_url: str, session: requests.Session):
        self.base = base_url
        self.session = session

    def encrypt(self, data, context: str) -> dict:
        """Encrypt a string or list of strings."""
        res = self.session.post(
            f"{self.base}/api/scca/vault/encrypt",
            json={"data": data, "context": context},
        )
        res.raise_for_status()
        return res.json()

    def decrypt(self, tokens: list, context: str) -> list:
        """Decrypt tokens back to plaintext."""
        res = self.session.post(
            f"{self.base}/api/scca/vault/decrypt",
            json={"tokens": tokens, "context": context},
        )
        res.raise_for_status()
        return res.json()["data"]

    def verify(self, tokens: list, merkle_root: str, context: str) -> dict:
        """Verify token integrity."""
        res = self.session.post(
            f"{self.base}/api/scca/vault/verify",
            json={
                "tokens": tokens,
                "merkleRoot": merkle_root,
                "context": context,
            },
        )
        res.raise_for_status()
        return res.json()


# ── Example: Encrypt logs before storage ──

vault = SCCAVault("https://your-scca-instance.com", authenticated_session)

# Encrypt sensitive log entries
result = vault.encrypt(
    data=[
        "User john@example.com logged in from 192.168.1.1",
        "Payment of $499.99 processed for order #12345",
        "API key sk_live_abc123 was rotated",
    ],
    context="audit-logs-2026"
)

# Store result["tokens"] and result["merkleRoot"] in your system
print(f"Encrypted {result['metadata']['itemCount']} items")
print(f"Compression ratio: {result['metadata']['compressionRatio']}")

# Later — verify and decrypt
check = vault.verify(stored_tokens, stored_merkle_root, "audit-logs-2026")
assert check["valid"], f"Integrity check failed: {check['errors']}"

entries = vault.decrypt(stored_tokens, "audit-logs-2026")
for entry in entries:
    print(f"[{entry['timestamp']}] {entry['content']}")
⚙
Use Cases
  • •PII Storage — Encrypt user data (SSN, addresses, payment info) before storing in your database
  • •Audit Logs — Encrypt sensitive log entries with tamper-proof Merkle verification
  • •Internal Chat — Add encryption to your existing chat system without rebuilding it
  • •Config Secrets — Encrypt API keys and credentials at rest with per-project isolation
  • •File Metadata — Encrypt file descriptions, tags, or annotations before cloud storage
  • •AI Pipelines — Encrypt prompts and responses in your AI workflow, verify they weren't modified in transit
integration

# Integration Guide

Use SCCA as the encrypted chat backend for your own application. Authenticate, create conversations, send messages, and handle streaming responses — all through the REST API.

> Authentication

SCCA uses NextAuth for session management. To call the API from an external client, first obtain a session by signing in via the credentials endpoint. The session cookie is used for all subsequent requests.

bash
# 1. Get CSRF token
CSRF=$(curl -s -c cookies.txt https://your-scca-instance.com/api/auth/csrf \
  | jq -r '.csrfToken')

# 2. Sign in with credentials
curl -s -b cookies.txt -c cookies.txt \
  -X POST https://your-scca-instance.com/api/auth/callback/credentials \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "email=user@example.com&password=yourpassword&csrfToken=$CSRF"

# 3. Now use cookies.txt for all API calls
curl -b cookies.txt https://your-scca-instance.com/api/scca/conversations

> Full Conversation Lifecycle (cURL)

Create a conversation, send a message, receive the streamed AI response, then edit a message with destructive editing.

bash
# Create a new conversation
CONV=$(curl -s -b cookies.txt \
  -X POST https://your-scca-instance.com/api/scca/conversations \
  -H "Content-Type: application/json" \
  -d '{"title": "My Integration Test"}')

CONV_ID=$(echo $CONV | jq -r '.id')
echo "Created conversation: $CONV_ID"

# Send a message (SSE streaming response)
curl -N -b cookies.txt \
  -X POST "https://your-scca-instance.com/api/scca/conversations/$CONV_ID/messages" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "What is SCCA?",
    "temperature": 0.7,
    "max_tokens": 4096
  }'
# Each line: data: {"token":"..."} ... data: {"done":true}

# Retrieve the full conversation with decrypted messages
curl -s -b cookies.txt \
  "https://your-scca-instance.com/api/scca/conversations/$CONV_ID"

# Destructive edit: rewrite message at sequence 0, regenerate AI response
curl -N -b cookies.txt \
  -X POST "https://your-scca-instance.com/api/scca/conversations/$CONV_ID/edit" \
  -H "Content-Type: application/json" \
  -d '{
    "sequence": 0,
    "content": "Explain SCCA encryption in detail",
    "regenerate": true
  }'

> JavaScript / TypeScript Client

Integrate SCCA into a Node.js backend or browser app. This example shows the full flow: auth, create, send, and stream.

typescript
const SCCA_BASE = "https://your-scca-instance.com";

// Helper: authenticated fetch (browser — cookies are sent automatically)
// For server-side, pass the session cookie from your auth flow.

async function createConversation(title?: string) {
  const res = await fetch(`${SCCA_BASE}/api/scca/conversations`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    credentials: "include",
    body: JSON.stringify({ title: title || "New Chat" }),
  });
  return res.json(); // { id, title, model, messageCount, ... }
}

async function sendMessage(
  conversationId: string,
  content: string,
  onToken: (token: string) => void,
  onDone: (data: { messageCount: number; title: string }) => void
) {
  const res = await fetch(
    `${SCCA_BASE}/api/scca/conversations/${conversationId}/messages`,
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      credentials: "include",
      body: JSON.stringify({
        content,
        temperature: 0.7,
        max_tokens: 8192,
      }),
    }
  );

  // Parse the SSE stream
  const reader = res.body!.getReader();
  const decoder = new TextDecoder();
  let buffer = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split("\n");
    buffer = lines.pop() || "";

    for (const line of lines) {
      if (!line.startsWith("data: ")) continue;
      const data = JSON.parse(line.slice(6));
      if (data.done) {
        onDone(data);
      } else if (data.token) {
        onToken(data.token);
      }
    }
  }
}

async function getMessages(conversationId: string) {
  const res = await fetch(
    `${SCCA_BASE}/api/scca/conversations/${conversationId}`,
    { credentials: "include" }
  );
  return res.json(); // { id, messages: [...], messageCount, ... }
}

async function destructiveEdit(
  conversationId: string,
  sequence: number,
  newContent: string,
  onToken: (token: string) => void,
  onDone: (data: any) => void
) {
  const res = await fetch(
    `${SCCA_BASE}/api/scca/conversations/${conversationId}/edit`,
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      credentials: "include",
      body: JSON.stringify({
        sequence,
        content: newContent,
        regenerate: true,
      }),
    }
  );

  // Same SSE parsing as sendMessage
  const reader = res.body!.getReader();
  const decoder = new TextDecoder();
  let buffer = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split("\n");
    buffer = lines.pop() || "";

    for (const line of lines) {
      if (!line.startsWith("data: ")) continue;
      const data = JSON.parse(line.slice(6));
      if (data.done) onDone(data);
      else if (data.token) onToken(data.token);
    }
  }
}

// ── Usage Example ──

async function main() {
  // 1. Create conversation
  const conv = await createConversation("Security Review");
  console.log("Created:", conv.id);

  // 2. Send a message, stream the response
  let response = "";
  await sendMessage(
    conv.id,
    "How does AES-256-GCM work?",
    (token) => {
      response += token;
      process.stdout.write(token); // stream to console
    },
    (data) => console.log("\nDone. Messages:", data.messageCount)
  );

  // 3. Edit message 0 and regenerate
  response = "";
  await destructiveEdit(
    conv.id,
    0,
    "Explain AES-256-GCM for a beginner",
    (token) => { response += token; },
    (data) => console.log("Edited. Messages:", data.messageCount)
  );
}

> Python Client

Integrate SCCA from a Python backend using requests.

python
import requests
import json

SCCA_BASE = "https://your-scca-instance.com"

class SCCAClient:
    def __init__(self, base_url: str):
        self.base = base_url
        self.session = requests.Session()

    def login(self, email: str, password: str):
        # Get CSRF token
        csrf = self.session.get(f"{self.base}/api/auth/csrf").json()["csrfToken"]
        # Sign in
        self.session.post(
            f"{self.base}/api/auth/callback/credentials",
            data={"email": email, "password": password, "csrfToken": csrf},
            allow_redirects=False,
        )

    def create_conversation(self, title: str = "New Chat") -> dict:
        res = self.session.post(
            f"{self.base}/api/scca/conversations",
            json={"title": title},
        )
        return res.json()

    def send_message(self, conv_id: str, content: str):
        """Send a message and yield streamed tokens."""
        res = self.session.post(
            f"{self.base}/api/scca/conversations/{conv_id}/messages",
            json={"content": content, "temperature": 0.7, "max_tokens": 8192},
            stream=True,
        )
        for line in res.iter_lines(decode_unicode=True):
            if not line or not line.startswith("data: "):
                continue
            data = json.loads(line[6:])
            if data.get("done"):
                yield {"done": True, **data}
                break
            elif "token" in data:
                yield {"token": data["token"]}

    def get_messages(self, conv_id: str) -> dict:
        return self.session.get(
            f"{self.base}/api/scca/conversations/{conv_id}"
        ).json()

    def destructive_edit(self, conv_id: str, sequence: int, content: str):
        """Edit a message and yield regenerated tokens."""
        res = self.session.post(
            f"{self.base}/api/scca/conversations/{conv_id}/edit",
            json={"sequence": sequence, "content": content, "regenerate": True},
            stream=True,
        )
        for line in res.iter_lines(decode_unicode=True):
            if not line or not line.startswith("data: "):
                continue
            data = json.loads(line[6:])
            if data.get("done"):
                yield {"done": True, **data}
                break
            elif "token" in data:
                yield {"token": data["token"]}


# ── Usage ──

client = SCCAClient(SCCA_BASE)
client.login("user@example.com", "yourpassword")

conv = client.create_conversation("Python Integration")
print(f"Created: {conv['id']}")

# Stream the AI response
for event in client.send_message(conv["id"], "What is SCCA?"):
    if "token" in event:
        print(event["token"], end="", flush=True)
    elif event.get("done"):
        print(f"\nDone. Messages: {event['messageCount']}")

# Retrieve full conversation
messages = client.get_messages(conv["id"])
for msg in messages["messages"]:
    print(f"[{msg['role']}] {msg['content'][:80]}")

> Handling SSE Streams

Both the /messages and /edit endpoints return Server-Sent Events. The format is simple:

text
data: {"token":"Hello"}         ← AI token (append to response)
data: {"token":" there"}        ← another token
data: {"token":"!"}             ← another token
data: {"done":true,"messageCount":4,"title":"Chat Title"}  ← stream complete

Error events:
data: {"error":"Unauthorized"}  ← auth failed
data: {"error":"Not found"}     ← conversation doesn't exist
⚠
Important Notes
  • •All encryption/decryption happens server-side. The API returns plaintext messages — you don't need to handle encryption in your client.
  • •Destructive edits are irreversible. Messages after the edit point are permanently deleted before the response is regenerated.
  • •The session cookie expires based on your NextAuth configuration. Re-authenticate if you receive 401 responses.
  • •Rate limiting is not enforced by default. If deploying publicly, add rate limiting middleware.
api

# API Reference

All endpoints require NextAuth session authentication. The user ID is extracted from the JWT session token.

> List Conversations

GET/api/scca/conversations

Returns all conversations for the authenticated user.

json
// Response: 200 OK
[
  {
    "id": "clx1234...",
    "title": "New Chat",
    "model": "llama-3.3-70b-versatile",
    "messageCount": 12,
    "createdAt": "2026-02-01T00:00:00.000Z",
    "updatedAt": "2026-02-01T12:00:00.000Z"
  }
]

> Create Conversation

POST/api/scca/conversations

Create a new encrypted conversation.

json
// Request Body
{
  "title": "Optional title",
  "model": "llama-3.3-70b-versatile"
}

// Response: 201 Created
{
  "id": "clx1234...",
  "title": "New Chat",
  "model": "llama-3.3-70b-versatile",
  "messageCount": 0,
  "createdAt": "2026-02-01T00:00:00.000Z",
  "updatedAt": "2026-02-01T00:00:00.000Z"
}

> Get Conversation

GET/api/scca/conversations/[id]?offset=0&limit=50

Retrieve conversation with decrypted messages. Supports viewport loading.

Query Parameters
offsetStarting message index (optional)
limitNumber of messages to return (optional)
json
// Response: 200 OK
{
  "id": "clx1234...",
  "title": "My Chat",
  "model": "llama-3.3-70b-versatile",
  "messageCount": 12,
  "messages": [
    {
      "id": "msg-0",
      "role": "user",
      "content": "Hello!",
      "sequence": 0,
      "timestamp": 1706745600
    },
    {
      "id": "msg-1",
      "role": "assistant",
      "content": "Hi there! How can I help?",
      "sequence": 1,
      "timestamp": 1706745601
    }
  ]
}

> Update Conversation

PATCH/api/scca/conversations/[id]

Rename conversation or change model.

json
// Request Body
{
  "title": "New Title",
  "model": "llama-3.1-8b-instant"
}

// Response: 200 OK

> Delete Conversation

DELETE/api/scca/conversations/[id]

Soft delete a conversation. Sets deletedAt timestamp.

json
// Response: 200 OK

> Send Message (Streaming)

POST/api/scca/conversations/[id]/messages

Send a message and receive AI response via Server-Sent Events stream.

json
// Request Body
{
  "content": "Explain quantum computing",
  "temperature": 0.7,
  "top_p": 1,
  "max_tokens": 8192,
  "model": "llama-3.3-70b-versatile",
  "systemPrompt": "You are a helpful AI assistant."
}

// Response: 200 OK (SSE Stream)
data: {"token":"Quantum"}
data: {"token":" computing"}
data: {"token":" is"}
...
data: {"done":true,"messageCount":4,"title":"Quantum Computing"}

> Destructive Edit

POST/api/scca/conversations/[id]/edit

Edit a message. All subsequent messages are permanently deleted. Optionally triggers AI regeneration.

json
// Edit with regeneration
{
  "sequence": 2,
  "content": "Updated message content",
  "regenerate": true,
  "temperature": 0.7,
  "systemPrompt": "You are a helpful assistant."
}

// Delete a message (and all after it)
{
  "action": "delete",
  "sequence": 4
}

// Response: SSE stream if regenerating, JSON otherwise
media

# Media Pipeline

SCCA v2 extends encryption to media files — images, video, audio, and documents. Each file passes through a format-aware pipeline: type detection, selective compression, AES-256-GCM encryption, and SCCA packet encapsulation with SHA-256 integrity verification.

> Format Support Matrix

Already-compressed formats (PNG, JPEG, MP4, MP3) are encrypted directly with no re-compression. Text-based formats (SVG, JSON, Markdown) get zlib level 9 compression before encryption for significant savings.

CategoryFormatsStrategyMax Size
ImagePNG, JPEG, WebP, HEICEncrypt only25 MB
ImageSVG, GIFzlib + encrypt25 MB
VideoMP4, WebM, MOVEncrypt only100 MB
AudioMP3, WAV, OGG, M4A, FLACEncrypt only50 MB
DocumentPDF, TXT, Markdown, JSONzlib-9 + encrypt10 MB

> SCCA Media Packet Format

Every media file is wrapped in a 70-byte-header SCCA v2 packet. The header is readable without decryption for routing and verification purposes.

text
SCCA Media Packet (v2):
┌──────────────────────────────────────────────────────┐
│ Magic Bytes (4 bytes)     "SCCA"                     │
│ Version (1 byte)          0x02                       │
│ Type Code (1 byte)        e.g. 0x01=PNG, 0x10=MP4   │
├──────────────────────────────────────────────────────┤
│ IV / Nonce (16 bytes)     Random, unique per file    │
│ Auth Tag (16 bytes)       AES-GCM authentication     │
│ Checksum (32 bytes)       SHA-256 of original data   │
├──────────────────────────────────────────────────────┤
│ Encrypted Payload         [compressed?] + encrypted  │
└──────────────────────────────────────────────────────┘

Type Codes:
  0x01 PNG   0x02 JPEG  0x03 WebP   0x04 GIF    0x05 SVG
  0x10 MP4   0x11 WebM  0x12 MOV
  0x20 MP3   0x21 WAV   0x22 OGG    0x23 M4A    0x24 FLAC
  0x30 PDF   0x40 TXT   0x41 MD     0x42 JSON

> Media Processing Pipeline

The pipeline detects format, applies selective compression, encrypts with the conversation key, and verifies integrity via SHA-256 checksum.

text
Input File
    │
    ▼
┌─────────────┐
│ Type Detect  │  Determine MIME type from extension / magic bytes
└──────┬──────┘
       │
       ▼
┌─────────────────────────────┐
│ Already compressed?          │
│  YES (PNG, JPEG, MP4, MP3)  │──▶ Skip compression
│  NO  (SVG, JSON, TXT, PDF)  │──▶ zlib level 9 deflate
└──────────────┬──────────────┘
               │
               ▼
┌──────────────────────┐
│ SHA-256 Checksum     │  Hash of original data for integrity
└──────────┬───────────┘
           │
           ▼
┌──────────────────────┐
│ AES-256-GCM Encrypt  │  Random 16-byte IV, conversation key
└──────────┬───────────┘
           │
           ▼
┌──────────────────────┐
│ SCCA Packet Build    │  70-byte header + encrypted payload
└──────────────────────┘

> Media API Endpoints

POST/api/scca/media

Upload a file. The server encrypts it through the SCCA media pipeline and stores the encrypted blob.

bash
# Upload a file (uses FormData)
curl -b cookies.txt \
  -X POST https://your-scca-instance.com/api/scca/media \
  -F "file=@photo.png" \
  -F "conversationId=clx1234..." \
  -F "messageSequence=2"

# Response:
{
  "id": "att_abc123",
  "originalName": "photo.png",
  "mimeType": "image/png",
  "originalSize": 485000,
  "encryptedSize": 485070,
  "compressionRatio": 1.0001,
  "compressionMethod": "none",
  "category": "image",
  "checksum": "a1b2c3..."
}
GET/api/scca/media?conversationId=xxx

List all media attachments for a conversation with aggregate statistics.

json
// Response: 200 OK
{
  "attachments": [
    {
      "id": "att_abc123",
      "originalName": "photo.png",
      "mimeType": "image/png",
      "originalSize": 485000,
      "encryptedSize": 485070,
      "category": "image",
      "createdAt": "2026-02-15T12:00:00.000Z"
    }
  ],
  "totals": {
    "count": 1,
    "originalBytes": 485000,
    "encryptedBytes": 485070,
    "avgCompressionRatio": 1.0001
  }
}
GET/api/scca/media/[id]

Decrypt and return the original file with its original Content-Type.

bash
# Download a decrypted file
curl -b cookies.txt \
  https://your-scca-instance.com/api/scca/media/att_abc123 \
  --output photo.png
DELETE/api/scca/media/[id]

Permanently delete an encrypted media attachment.

json
// Response: 200 OK
{ "deleted": true }

> JavaScript: Upload Media

typescript
async function uploadMedia(
  file: File,
  conversationId: string,
  messageSequence: number
) {
  const formData = new FormData();
  formData.append("file", file);
  formData.append("conversationId", conversationId);
  formData.append("messageSequence", String(messageSequence));

  const res = await fetch("/api/scca/media", {
    method: "POST",
    credentials: "include",
    body: formData,
  });
  return res.json();
  // { id, originalName, mimeType, compressionRatio, ... }
}
▸
Compression Strategy

Already-compressed formats (PNG, JPEG, MP4, MP3) use encrypt-only mode with ~70 bytes overhead for the SCCA header. Attempting to re-compress these formats would waste CPU cycles for zero savings. Text-based formats (SVG, JSON, Markdown, PDF) use zlib level 9 before encryption, achieving 50-90% compression. The pipeline automatically selects the right strategy based on MIME type.

crypto

# Crypto Engine

All encryption is server-side. The master key never leaves server memory. Keys are derived on-demand via HKDF-SHA256 and never stored.

> Key Hierarchy

text
MASTER_KEY_SECRET (env var, 32 bytes)
    │
    ├── HKDF("user-key", masterKey + userSalt) → User Key
    │       │
    │       ├── HKDF("conv-key", userKey + conversationId) → Conversation Key
    │       │       └── Used for AES-256-GCM encrypt/decrypt
    │       │
    │       └── HKDF("integrity", userKey + conversationId) → Integrity Key
    │               └── Used for Merkle tree HMAC
    │
    └── Never stored. Only in server memory.

> Key Functions

getServerMasterKey(): Buffer

Returns the 32-byte master key from MASTER_KEY_SECRET env var.

deriveUserKey(masterKey, userSalt): Buffer

HKDF-SHA256: masterKey + userSalt → 32-byte user key.

deriveConversationKey(userKey, conversationId): Buffer

HKDF-SHA256: userKey + conversationId → 32-byte conversation key.

deriveIntegrityKey(userKey, conversationId): Buffer

HKDF-SHA256: userKey + conversationId + "integrity" → 32-byte integrity key.

> Merkle Tree Verification

text
hash[0] = HMAC(integrityKey, token[0])
hash[1] = HMAC(integrityKey, hash[0] + token[1])
hash[2] = HMAC(integrityKey, hash[1] + token[2])
...
merkleRoot = hash[N-1]

If any token is modified, the entire Merkle root changes, detecting tampering.

> Security Properties

PropertyGuarantee
ConfidentialityAES-256-GCM — computationally infeasible without key
AuthenticityGCM auth tag — tampering detected
IntegrityMerkle root — any modification detected
Key IsolationPer-conversation keys via HKDF
Nonce SafetyRandom 12-byte nonce per encryption
binary

# Binary Message Format

Each message is packed into a compact binary format before encryption. The 10-byte header can be read without decrypting (via peekMessageHeader).

text
Binary layout of a packed message:
┌────────────────────────────────────────────────┐
│ Header (10 bytes)                              │
│  [version:1][role:1][sequence:2][timestamp:4]  │
│  [flags:2]                                     │
├────────────────────────────────────────────────┤
│ Nonce (12 bytes) - random, never reused        │
├────────────────────────────────────────────────┤
│ Ciphertext (variable)                          │
│  AES-256-GCM(conversationKey, nonce,           │
│    zlib.deflate(content))                      │
├────────────────────────────────────────────────┤
│ Auth Tag (16 bytes) - GCM authentication       │
└────────────────────────────────────────────────┘

> Operations

OperationDescription
packPlaintext → binary header + zlib compress + AES encrypt → base64 token
unpackBase64 token → AES decrypt + decompress → plaintext + metadata
appendPack a new message and add to the conversation token array
truncateRemove all tokens after a given sequence number
peekRead the 10-byte header without decrypting content

> Role Values

RoleByteDescription
system0x00System prompt / context message
user0x01User-authored message
assistant0x02AI-generated response
vocabulary

# Vocabulary

These terms have exact, unambiguous meanings within SCCA.

Destructive Edit

Edit operation that permanently deletes all messages after the edit point and replaces the target message. Not reversible. Not a branch.

Conversation Row

A single PostgreSQL row containing the entire conversation: encrypted token array, metadata, and integrity hash. Not a collection of message rows.

Master Key

32-byte server-side secret (MASTER_KEY_SECRET env var). Root of all key derivation. Never stored in the database.

User Key

Derived from master key + user salt via HKDF. Unique per user. Never stored anywhere.

Conversation Key

Derived from user key + conversation ID via HKDF. Used for AES-256-GCM encrypt/decrypt. Unique per conversation.

Integrity Key

Derived from user key + conversation ID + "integrity" context. Used only for Merkle tree HMAC, not encryption.

Message Token

Base64-encoded encrypted blob in the messageTokens array. Contains: header + compressed ciphertext + nonce + auth tag. Not a JWT.

Binary Header

First 10 bytes of a packed message: version, role, sequence, timestamp, flags. Readable without decryption.

Merkle Root

HMAC-SHA256 chain hash across all message tokens. One value for the entire conversation. Detects any tampering.

Viewport

A windowed subset of messages loaded by the client (e.g., messages 40-60 of 100). Enables efficient loading.

SCCA v2.0 — Open Source
Back to Home