# 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
# Quick Start
Get SCCA running locally in minutes.
> 1. Clone and Install
git clone https://github.com/Vii-Hunnid/SCCA.git
cd SCCA
npm install> 2. Environment Variables
# .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
npx prisma generate
npx prisma db push> 4. Run
npm run devOpen http://localhost:3000 and register an account to start.
# 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:
# Install dependencies
npm install @tanstack/react-query # Optional: for data fetching// 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,
});// 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:
# Install dependencies
npm install @vueuse/core # Optional: for utilities// 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,
};
}<!-- 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:
# Install dependencies
npm install @react-native-async-storage/async-storage// 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();// 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:
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
http: ^1.2.0
shared_preferences: ^2.2.2
uuid: ^4.3.3// 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
}
}
}
}
}
}// 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:
// 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:
// 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")
}// 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()
}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 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.
# 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"}'| Endpoint | Description |
|---|---|
POST /api/scca/keys | Generate a new API key (session auth only) |
GET /api/scca/keys | List your active keys (session auth only) |
DELETE /api/scca/keys/[id] | Revoke an API key (session auth only) |
// 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."
}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
/api/scca/vault/encryptEncrypt one or more strings. Returns encrypted tokens and a Merkle integrity root.
// 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
/api/scca/vault/decryptDecrypt tokens back to plaintext. Must use the same context as encryption.
// 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
/api/scca/vault/verifyVerify that tokens haven't been tampered with using the Merkle-HMAC chain.
// 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)
# 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
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
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']}")- •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 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.
# 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.
# 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.
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.
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:
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- •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 Reference
All endpoints require NextAuth session authentication. The user ID is extracted from the JWT session token.
> List Conversations
/api/scca/conversationsReturns all conversations for the authenticated user.
// 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
/api/scca/conversationsCreate a new encrypted conversation.
// 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
/api/scca/conversations/[id]?offset=0&limit=50Retrieve conversation with decrypted messages. Supports viewport loading.
offsetStarting message index (optional)limitNumber of messages to return (optional)// 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
/api/scca/conversations/[id]Rename conversation or change model.
// Request Body
{
"title": "New Title",
"model": "llama-3.1-8b-instant"
}
// Response: 200 OK> Delete Conversation
/api/scca/conversations/[id]Soft delete a conversation. Sets deletedAt timestamp.
// Response: 200 OK> Send Message (Streaming)
/api/scca/conversations/[id]/messagesSend a message and receive AI response via Server-Sent Events stream.
// 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
/api/scca/conversations/[id]/editEdit a message. All subsequent messages are permanently deleted. Optionally triggers AI regeneration.
// 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 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.
| Category | Formats | Strategy | Max Size |
|---|---|---|---|
| Image | PNG, JPEG, WebP, HEIC | Encrypt only | 25 MB |
| Image | SVG, GIF | zlib + encrypt | 25 MB |
| Video | MP4, WebM, MOV | Encrypt only | 100 MB |
| Audio | MP3, WAV, OGG, M4A, FLAC | Encrypt only | 50 MB |
| Document | PDF, TXT, Markdown, JSON | zlib-9 + encrypt | 10 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.
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.
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
/api/scca/mediaUpload a file. The server encrypts it through the SCCA media pipeline and stores the encrypted blob.
# 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..."
}/api/scca/media?conversationId=xxxList all media attachments for a conversation with aggregate statistics.
// 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
}
}/api/scca/media/[id]Decrypt and return the original file with its original Content-Type.
# Download a decrypted file
curl -b cookies.txt \
https://your-scca-instance.com/api/scca/media/att_abc123 \
--output photo.png/api/scca/media/[id]Permanently delete an encrypted media attachment.
// Response: 200 OK
{ "deleted": true }> JavaScript: Upload Media
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, ... }
}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 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
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(): BufferReturns the 32-byte master key from MASTER_KEY_SECRET env var.
deriveUserKey(masterKey, userSalt): BufferHKDF-SHA256: masterKey + userSalt → 32-byte user key.
deriveConversationKey(userKey, conversationId): BufferHKDF-SHA256: userKey + conversationId → 32-byte conversation key.
deriveIntegrityKey(userKey, conversationId): BufferHKDF-SHA256: userKey + conversationId + "integrity" → 32-byte integrity key.
> Merkle Tree Verification
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
| Property | Guarantee |
|---|---|
| Confidentiality | AES-256-GCM — computationally infeasible without key |
| Authenticity | GCM auth tag — tampering detected |
| Integrity | Merkle root — any modification detected |
| Key Isolation | Per-conversation keys via HKDF |
| Nonce Safety | Random 12-byte nonce per encryption |
# 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).
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
| Operation | Description |
|---|---|
pack | Plaintext → binary header + zlib compress + AES encrypt → base64 token |
unpack | Base64 token → AES decrypt + decompress → plaintext + metadata |
append | Pack a new message and add to the conversation token array |
truncate | Remove all tokens after a given sequence number |
peek | Read the 10-byte header without decrypting content |
> Role Values
| Role | Byte | Description |
|---|---|---|
| system | 0x00 | System prompt / context message |
| user | 0x01 | User-authored message |
| assistant | 0x02 | AI-generated response |
# Vocabulary
These terms have exact, unambiguous meanings within SCCA.
Edit operation that permanently deletes all messages after the edit point and replaces the target message. Not reversible. Not a branch.
A single PostgreSQL row containing the entire conversation: encrypted token array, metadata, and integrity hash. Not a collection of message rows.
32-byte server-side secret (MASTER_KEY_SECRET env var). Root of all key derivation. Never stored in the database.
Derived from master key + user salt via HKDF. Unique per user. Never stored anywhere.
Derived from user key + conversation ID via HKDF. Used for AES-256-GCM encrypt/decrypt. Unique per conversation.
Derived from user key + conversation ID + "integrity" context. Used only for Merkle tree HMAC, not encryption.
Base64-encoded encrypted blob in the messageTokens array. Contains: header + compressed ciphertext + nonce + auth tag. Not a JWT.
First 10 bytes of a packed message: version, role, sequence, timestamp, flags. Readable without decryption.
HMAC-SHA256 chain hash across all message tokens. One value for the entire conversation. Detects any tampering.
A windowed subset of messages loaded by the client (e.g., messages 40-60 of 100). Enables efficient loading.

