Skip to main content

Integration Matrix: family-hub + devarno.cloud Services

Generated: 2026-02-19
Status: TASKSET 2 Deliverable

Overview

This document is the source of truth for all external service integrations from family-hub (casa.devarno.cloud) to surrounding devarno.cloud services. Every external call path maps to a documented contract.

Service Integration Matrix

ServiceStatusProtocolAuth ModelBase URL Env VarContract Source
chat-serviceImplementedHTTP REST + WebSocketCookie forwarding / Query tokenCHAT_SERVICE_URL, NEXT_PUBLIC_CHAT_WS_URLproto-contracts/proto/devarno/events/v1/chat.proto
media-serviceImplementedHTTP REST (multipart)Cookie forwardingMEDIA_SERVICE_URLproto-contracts/proto/devarno/content/v1/media.proto
auth-serviceScaffoldingHTTP RESTN/A (family-hub uses better-auth directly)N/Aproto-contracts/proto/devarno/shared/v1/user.proto
api-gatewayScaffoldingHTTPJWT (planned)N/AN/A
audit-serviceScaffoldingRedis Streams (planned)InternalN/Aproto-contracts/proto/devarno/events/v1/audit.proto
config-serviceScaffoldingHTTP + Redis pub/sub (planned)CookieN/AN/A
V01T CMSImplementedHTTP REST (public API)Public (no auth)NEXT_PUBLIC_V01T_API_URL, V01T_API_URLSee V01T Integrations below

Detailed Endpoint Contracts

1. chat-service (Rust/Axum)

Base URLs:
  • REST: ${CHAT_SERVICE_URL} (default: http://localhost:8082)
  • WebSocket: ${NEXT_PUBLIC_CHAT_WS_URL} (default: ws://localhost:8082)
Authentication:
  • HTTP endpoints: Cookie header forwarded verbatim
  • WebSocket: Query parameter ?token=<better-auth.session_token>
  • Validation: GET ${AUTH_SERVICE_URL}/api/auth/get-session with Cookie header

Endpoints

MethodPathfamily-hub CallerRequestResponseErrors
GET/ws?token=<token>use-chat-socket.tsWebSocket upgradeWS connection401 invalid token
GET/api/v1/messages/api/chat-service/messages/route.ts?channel=<id>&before=<ts>&limit=<n>ChatMessage[]401, 400
GET/api/v1/presenceNot currently used?channel=<id>PresenceUpdate[]401

WebSocket Message Contract (JSON)

Client → Server:
// Send message
{ type: "message", content: string, channel: string }

// Typing indicator
{ type: "typing", channel: string, typing: boolean }

// Join/leave channel
{ type: "join", channel: string }
{ type: "leave", channel: string }

// Keepalive
{ type: "ping" }
Server → Client:
// New message
{ 
  type: "message", 
  id: string, 
  sender: string,        // user_id
  content: string, 
  channel: string, 
  timestamp: string      // ISO8601
}

// Presence update
{ 
  type: "presence", 
  user_id: string, 
  online: boolean, 
  channel: string 
}

// Typing indicator
{ 
  type: "typing", 
  user_id: string, 
  channel: string, 
  typing: boolean 
}

// Keepalive response
{ type: "pong" }

// Error
{ type: "error", code: string, message: string }

Proto Contract Mapping

Proto Messagechat-service JSON Fieldfamily-hub Type
ChatMessage.ididstring
ChatMessage.contentcontentstring
ChatMessage.sender_idsender (WS) / sender_id (REST)string
ChatMessage.channel_idchannel (WS) / channel_id (REST)string
ChatMessage.created_attimestamp (WS) / created_at (REST)string (ISO8601)
Gap: family-hub defines its own ChatMessage interface in modules/chat/service.ts:13-21 and hooks/use-chat-socket.ts:11-19 instead of importing from proto-contracts/gen/ts/.

2. media-service (Go/Gin)

Base URL: ${MEDIA_SERVICE_URL} (default: http://localhost:8081) Authentication: Cookie header forwarded verbatim

Endpoints

MethodPathfamily-hub CallerRequestResponseErrors
POST/api/v1/upload/api/media/upload/route.tsmultipart/form-data with file, optional typeUploadResponse401, 400, 413, 415
GET/api/v1/files/:idNot proxied-MediaMetadata401, 404
GET/api/v1/files/:id/downloadDirect URL in content-Binary file401, 404
GET/api/v1/files/:id/download?thumb=1Direct URL in content-Binary thumbnail401, 404
DELETE/api/v1/files/:idNot proxied-204401, 403, 404

Upload Response Contract

// POST /api/v1/upload response
interface UploadResponse {
  id: string;
  url: string;           // Download URL
  thumbnail_url: string; // Thumbnail URL (if applicable)
  sha256: string;        // Content hash
  size_bytes: number;
}

Proto Contract Mapping

Proto Messagemedia-service JSON FieldNotes
UploadResponse.ididMatch
UploadResponse.urlurlMatch
UploadResponse.thumbnail_urlthumbnail_urlMatch
UploadResponse.sha256sha256Match
UploadResponse.size_bytessize_bytesProto is int64, JSON is number

Restrictions

  • Allowed content types: image/jpeg, image/png, image/gif, image/webp, application/pdf, video/mp4, video/webm
  • Max file size: 10MB (configurable via MAX_FILE_SIZE)
  • Max avatar size: 2MB (configurable via MAX_AVATAR_SIZE)
  • Role child cannot upload files

3. better-auth (Internal to family-hub)

family-hub does not call an external auth-service. Authentication is handled internally via better-auth library with the following configuration: Session Validation Endpoint (exposed by better-auth):
GET /api/auth/get-session
Headers: Cookie: better-auth.session_token=<token>

Response (200):
{
  "user": {
    "id": string,
    "email": string,
    "name": string,
    "image": string | null,
    "role": "admin" | "member" | "child",
    "emailVerified": boolean,
    "banned": boolean,
    "createdAt": string,
    "updatedAt": string
  },
  "session": {
    "id": string,
    "userId": string,
    "expiresAt": string,
    "ipAddress": string,
    "userAgent": string
  }
}

Response (401):
{ "user": null, "session": null }
This endpoint is called by:
  • chat-servicesrc/auth/client.rs:24-46
  • media-serviceinternal/services/auth_client.go:27-55
Cross-subdomain cookies enabled:
  • Domain: .devarno.cloud
  • SameSite: lax
  • Secure: true (production)

Call Path Inventory

family-hub → chat-service

family-hub LocationTargetAuthPurpose
src/hooks/use-chat-socket.ts:236-238${CHAT_WS_URL}/ws?token=<token>Query param (session token)WebSocket realtime messages
src/hooks/use-chat-socket.ts:139-141/api/chat-service/messages (internal)CredentialsLoad message history
src/app/api/chat-service/messages/route.ts:37-44${CHAT_SERVICE_URL}/api/v1/messagesCookie forwardingProxy to chat-service

family-hub → media-service

family-hub LocationTargetAuthPurpose
src/app/api/media/upload/route.ts:24-30${MEDIA_SERVICE_URL}/api/v1/uploadCookie forwardingUpload files

family-hub Internal (Direct DB)

family-hub LocationData SourcePurpose
src/modules/chat/service.tsDrizzle → messages, users tablesPolling fallback message storage
src/modules/chat/presence.tsIn-memory Map (BROKEN on Vercel)Presence tracking
src/app/api/chat/messages/route.tsmodules/chat/service.tsPolling endpoint
src/app/api/chat/send/route.tsmodules/chat/service.tsSend message (polling fallback)
src/app/api/chat/presence/route.tsmodules/chat/presence.tsPresence heartbeat

Gap Analysis

Critical Gaps

GapImpactResolution
In-memory presencePresence data not shared across Vercel instancesTASKSET 4: Move to Upstash Redis
Duplicate message storageMessages stored in both family-hub DB and chat-service DBDecide single source of truth
No proto-contracts usagefamily-hub defines own types; drift riskImport from proto-contracts/gen/ts/
No request IDsCannot trace requests across servicesAdd X-Request-ID / traceparent header
No timeoutsProxy routes have no explicit fetch timeoutAdd 10s timeout to all outbound fetches
No retriesSingle failure = user errorAdd retry with exponential backoff for idempotent calls

Architecture Gaps

GapDescriptionResolution
WS auth via raw session tokenSession token passed in query param to chat-serviceTASKSET 1 decision: Implement ephemeral tokens
No unified error envelopeEach route returns different error shapesStandardize: { error: string, code?: string }
Polling without backoffFixed 3s interval regardless of errorsAdd exponential backoff + jitter
Direct cookie parsing in JSuse-chat-socket.ts:79-83 parses document.cookieFetch token from server endpoint

Environment Variables (Complete)

Required

VariableServiceDescription
DATABASE_URLfamily-hubNeon PostgreSQL connection string
BETTER_AUTH_SECRETfamily-hubAuth encryption key (min 32 chars)
BETTER_AUTH_URLfamily-hubAuth base URL (e.g., https://casa.devarno.cloud)
RESEND_API_KEYfamily-hubResend email service API key
NEXT_PUBLIC_APP_URLfamily-hubClient-side app URL
MEDIA_SERVICE_URLfamily-hubMedia service backend URL
CHAT_SERVICE_URLfamily-hubChat service REST URL
NEXT_PUBLIC_CHAT_WS_URLfamily-hubChat service WebSocket URL

New (Required for Production)

VariableServiceDescription
UPSTASH_REDIS_REST_URLfamily-hubUpstash Redis endpoint
UPSTASH_REDIS_REST_TOKENfamily-hubUpstash Redis auth token
WS_TOKEN_SECRETfamily-hubSecret for signing ephemeral WS tokens

External Service Dependencies

ServiceRequired Env Vars
chat-serviceDATABASE_URL, REDIS_URL, AUTH_SERVICE_URL, PORT
media-serviceDATABASE_URL, AUTH_SERVICE_URL, PORT, STORAGE_PATH, BASE_URL
Note: AUTH_SERVICE_URL for chat-service and media-service should point to family-hub’s /api/auth/get-session endpoint (e.g., https://casa.devarno.cloud).

CORS / Origins Configuration

family-hub Trusted Origins

From src/lib/auth.ts:77-81:
trustedOrigins: [
  'https://casa.devarno.cloud',
  'https://*.devarno.cloud',
  ...(process.env.NODE_ENV === 'development' ? ['http://localhost:3000'] : []),
]

Security Headers

From next.config.ts:
  • X-Frame-Options: DENY
  • Content-Security-Policy: connect-src 'self' wss: ws: (allows WebSocket)
  • Strict-Transport-Security: max-age=31536000; includeSubDomains

WebSocket CORS

chat-service must allow WebSocket upgrades from:
  • https://casa.devarno.cloud
  • http://localhost:3000 (development)

Proto Contract File Locations

DomainProto FileGenerated TypeScript
Chatproto-contracts/proto/devarno/events/v1/chat.protoproto-contracts/gen/ts/devarno/events/v1/chat_pb.ts
Mediaproto-contracts/proto/devarno/content/v1/media.protoproto-contracts/gen/ts/devarno/content/v1/media_pb.ts
Userproto-contracts/proto/devarno/shared/v1/user.protoproto-contracts/gen/ts/devarno/shared/v1/user_pb.ts
Auditproto-contracts/proto/devarno/events/v1/audit.protoproto-contracts/gen/ts/devarno/events/v1/audit_pb.ts

How to Regenerate

cd proto-contracts
buf generate

Failure Modes and Error Handling

chat-service Unavailable

ScenarioCurrent BehaviorCorrect Behavior
WS connection failsExponential backoff (1s, 2s, 4s), then pollingAcceptable
Polling 500 errorSilent failure, retry in 3sLog, exponential backoff
Auth validation failsWS upgrade rejectedRedirect to sign-in

media-service Unavailable

ScenarioCurrent BehaviorCorrect Behavior
Upload failsGeneric “Failed to upload file”Show service-specific error, suggest retry
TimeoutNo timeout configured30s timeout for uploads, user feedback

Database Unavailable

ScenarioCurrent BehaviorCorrect Behavior
Neon connection fails500 “Internal server error”Graceful degradation message
Health check failsReturns unhealthy statusAcceptable

Verification Checklist

  • Every external fetch() in family-hub documented above
  • Every external fetch() has explicit timeout
  • All required env vars listed
  • All proto-contracts mapped to actual payloads
  • Error envelopes consistent across routes
  • Request ID propagation implemented
  • Gaps identified have resolution tasks

V01T CMS Integration

Overview

V01T is a content management system used to serve dynamic data across Devarno applications. It provides two primary integrations:
  1. Devarno Ecosystem (NodeData) — 18 GitHub organizations
  2. Flight Path (ArtefactData) — Blog articles and content

Base URLs

  • Public API: ${NEXT_PUBLIC_V01T_API_URL} or ${V01T_API_URL} (e.g., http://localhost:8000)
  • Endpoint pattern: /trace/{app_slug}/{component_type}/

Integration: devarno-ecosystem (NodeData)

Application slug: devarno-ecosystem
Component type: node
Data source: DEVARNO.json (18 organizations)

Endpoints

MethodPathClientRequestResponseCache
GET/trace/devarno-ecosystem/node/devarno-landing (dashboard)Pagination: ?page=1&page_size=50NodeData[] (orgs with metadata)2 min
GET/trace/devarno-ecosystem/node/?search=sparkidevarno-landing (search)Search queryFiltered NodeData[]2 min
GET/trace/devarno-ecosystem/node/?cluster=devarno-orgsAdmin APICluster filterAll orgs in cluster2 min

Response Structure

interface ListResponse {
  success: boolean;
  data: Array<{
    title: string;           // Org name (e.g., "sparki-tools")
    slug: string;            // URL-safe identifier
    cluster: string;         // Always "devarno-orgs"
    _meta: {
      external_id: string;   // GitHub URL
      last_synced: string;   // ISO8601 timestamp
    };
    // raw_data fields spread into response:
    url: string;             // GitHub org URL
    website: string;         // Official website
    description: string;     // Short description
    stack: string[];         // Tech stack (Go, TypeScript, etc.)
    repo_count: number;
    repos: string[];         // Repository names
    highlights: string[];    // Key features
    status: string;          // "active" | "archived" | "planned"
  }>;
  metadata: {
    application: string;
    component_type: string;
    total_count: number;
    filtered_count: number;
    page: number;
    page_size: number;
    total_pages: number;
    has_next: boolean;
    has_previous: boolean;
  };
  timestamp: string;         // ISO8601
}

Frontend Integration (devarno-landing)

// src/content/devarno-data-loader.ts
import { createV01tClient } from '@v01t/client';

export async function loadDevarnoData(): Promise<DevarnoData> {
  try {
    const client = createV01tClient({
      baseUrl: process.env.V01T_API_URL,
    });
    
    const response = await client.getNodes('devarno-ecosystem', {
      page_size: 50,
    });
    
    return transformNodeDataToDevarnoData(response.data);
  } catch (error) {
    // Fallback to static data if V01T unavailable
    return getStaticDevarnoData();
  }
}

// src/app/dashboard/page.tsx (server component)
export default async function DashboardPage() {
  const data = await loadDevarnoData();
  return <Dashboard data={data} />;
}

Data Refresh

Manual:
python api/manage.py seed_ecosystem --skip-existing=false
Automated: Scheduled n8n workflow monitors DEVARNO.json changes and calls seed command.

Integration: flight-path (ArtefactData)

Application slug: flight-path
Component type: artefact
Data source: MOCK_ARTICLES (4 blog articles)

Endpoints

MethodPathClientRequestResponseCache
GET/trace/flight-path/artefact/devarno-landing (blog list)Pagination + sort: ?page=1&page_size=20&order_by=-priorityArtefactData[] (articles)2 min
GET/trace/flight-path/artefact/?search=skyflowdevarno-landing (search)Search queryFiltered ArtefactData[]2 min
GET/trace/flight-path/artefact/?search=publishedAdmin APIStatus filterPublished articles2 min

Response Structure

interface ListResponse {
  success: boolean;
  data: Array<{
    title: string;              // Article headline
    slug: string;               // URL-safe identifier
    cluster: string;            // Always "flight-path-articles"
    _meta: {
      external_id: string;      // Notion page ID
      last_synced: string;      // ISO8601 timestamp
    };
    // raw_data fields spread into response:
    id: string;                 // Notion page ID (for content fetch)
    excerpt: string;            // Short summary
    publishedAt: string;        // ISO8601 timestamp
    status: string;             // "draft" | "published" | "archived"
    priority: number;           // Publication order
    conversion_intent: string;  // "engagement" | "education" | "lead_gen"
    business_value_score: number; // 0-10
    word_count: number;
    readTime: string;           // e.g., "15min"
    tags: string[];
    keywords: string[];
    isPremium: boolean;
    author: {
      name: string;
      avatar: string;           // Image path
    };
  }>;
  metadata: {
    application: string;
    component_type: string;
    total_count: number;
    filtered_count: number;
    page: number;
    page_size: number;
    total_pages: number;
    has_next: boolean;
    has_previous: boolean;
  };
  timestamp: string;
}

Frontend Integration (devarno-landing)

Article list:
// src/lib/articles.ts
export class ArticlesService {
  static async getArticles() {
    try {
      const response = await fetch(
        `${process.env.V01T_API_URL}/trace/flight-path/artefact/?page_size=50`,
        { next: { revalidate: 120 } }
      );
      const json = await response.json();
      return json.data.map(convertArtefactToArticle);
    } catch {
      return MOCK_ARTICLES;
    }
  }

  static async getArticle(slug: string) {
    const articles = await this.getArticles();
    return articles.find(a => a.slug === slug);
  }
}

function convertArtefactToArticle(artefact: Artefact): Article {
  return {
    id: artefact.raw_data.id,        // Notion page ID
    slug: artefact.slug,
    title: artefact.title,
    excerpt: artefact.raw_data.excerpt,
    publishedAt: artefact.raw_data.publishedAt,
    readTime: artefact.raw_data.readTime,
    author: artefact.raw_data.author,
    tags: artefact.raw_data.tags,
  };
}
Article rendering:
// src/app/blog/[slug]/page.tsx (server component)
import { ArticlesService } from '@/lib/articles';
import { getNotionPage } from '@/lib/notion';

export default async function BlogPage({ params }: { params: { slug: string } }) {
  // Fetch metadata from V01T
  const article = await ArticlesService.getArticle(params.slug);
  if (!article) notFound();

  // Fetch content from Notion (article.id = Notion page ID)
  const content = await getNotionPage(article.id);

  return (
    <article>
      <h1>{article.title}</h1>
      <RichText blocks={content.blocks} />
    </article>
  );
}

Data Refresh

Manual:
python api/manage.py seed_flight_path --skip-existing=false
Automated: n8n monitors article status changes and updates metadata.

Environment Variables

VariableServiceDescriptionExample
NEXT_PUBLIC_V01T_API_URLdevarno-landingPublic V01T API URL (browser)https://v01t.devarno.cloud
V01T_API_URLdevarno-landingV01T API URL (server-side)http://v01t-api:8000

Error Handling

V01T uses a consistent error envelope:
interface ErrorResponse {
  success: false;
  error: string;              // Human-readable message
  code?: string;              // Machine-readable code
  details?: Record<string, any>;
}
Common errors:
StatusCodeMeaningHandling
404not_foundApplication or component type doesn’t existReturn 404 to user, log to monitoring
400invalid_paramsBad pagination or filter syntaxFallback to defaults, log to Sentry
500internal_errorServer errorReturn cached data or MOCK_ARTICLES
Network timeoutN/AV01T unreachableReturn MOCK_ARTICLES, retry after 5s
Client error handling:
import { V01tApiError, V01tNetworkError } from '@v01t/client';

try {
  const response = await client.getNodes('devarno-ecosystem');
} catch (error) {
  if (error instanceof V01tApiError) {
    console.error('V01T API error:', error.code, error.message);
    return getCachedData() || MOCK_DATA;
  } else if (error instanceof V01tNetworkError) {
    console.error('V01T network error:', error.message);
    return getLocalData();
  }
}

Call Path Inventory

devarno-landing LocationTargetAuthPurpose
src/app/dashboard/page.tsx/trace/devarno-ecosystem/node/PublicDashboard: Load 18 orgs
src/lib/articles.ts:getArticles()/trace/flight-path/artefact/PublicBlog: Load article list
src/lib/articles.ts:searchArticles()/trace/flight-path/artefact/?search=...PublicBlog: Search articles
src/app/blog/[slug]/page.tsx/trace/flight-path/artefact/?search={slug}PublicBlog: Fetch single article metadata

Cache Strategy

All V01T API responses are cached for 2 minutes (configurable):
// Server-side caching
const response = await fetch(url, { next: { revalidate: 120 } });

// On-demand revalidation
revalidateTag('v01t-organizations');
revalidateTag('v01t-articles');
Cache invalidation triggers:
  • Automatic after 2 min
  • Manual via n8n webhook after seed command completes
  • User action: “Publish article” or “Update organization”

Monitoring

Metrics to track:
  • Request latency: V01T_API_LATENCY_MS (p50, p99)
  • Error rate: V01T_API_ERRORS (by error code)
  • Cache hit rate: V01T_CACHE_HIT_RATE
  • Data staleness: MAX_ORGANIZATION_AGE_HOURS, MAX_ARTICLE_AGE_HOURS
Alerts:
  • V01T API response time > 2s
  • V01T error rate > 5% in 5-min window
  • All cached data older than 24h (stale data indicator)

Next Steps (TASKSET 3)

  1. Create unified service client layer (src/lib/services/)
  2. Import types from proto-contracts/gen/ts/
  3. Add request ID generation and propagation
  4. Implement timeouts and retry logic
  5. Create ephemeral WS token endpoint
  6. Replace in-memory presence with Upstash Redis