BISO Sites

Data Flow

How data flows through the BISO Sites monorepo - from user interaction to database and back.

Data Flow

Understanding how data moves through the system is crucial for building features and debugging issues. This document explains the various data flow patterns used in BISO SItes.

Data Flow Patterns

BISO SItes uses different patterns depending on the operation type:

  1. Server Component Query - Read data in Server Components
  2. Server Action Mutation - Write data with Server Actions
  3. API Route Handler - External webhooks and callbacks
  4. Client-Side Fetch - Dynamic client-side data

Server Component Data Flow (Reads)

The most common pattern for displaying data.

Example

// app/posts/page.tsx - Server Component
import { createSessionClient } from '@repo/api/server';

export default async function PostsPage() {
  // Fetch data directly in Server Component
  const { db } = await createSessionClient();
  const posts = await db.listDocuments(
    'database_id',
    'posts_collection'
  );

  return (
    <div>
      {posts.documents.map(post => (
        <PostCard key={post.$id} post={post} />
      ))}
    </div>
  );
}

Benefits:

  • ✅ No loading states needed
  • ✅ SEO friendly
  • ✅ Fast initial render
  • ✅ Direct database access
  • ✅ Automatic deduplication

Use When:

  • Displaying content
  • SEO is important
  • Data doesn't change frequently

Server Action Data Flow (Writes)

Used for mutations like creating, updating, deleting data.

Example

// app/actions/posts.ts - Server Action
'use server';

import { createSessionClient } from '@repo/api/server';
import { revalidatePath } from 'next/cache';

export async function createPost(formData: FormData) {
  const { db } = await createSessionClient();
  
  const post = await db.createDocument(
    'database_id',
    'posts_collection',
    'unique()',
    {
      title: formData.get('title'),
      content: formData.get('content'),
      publishedAt: new Date().toISOString(),
    }
  );
  
  // Revalidate the posts page to show new post
  revalidatePath('/posts');
  
  return { success: true, postId: post.$id };
}
// components/create-post-form.tsx - Client Component
'use client';

import { createPost } from '@/app/actions/posts';
import { Button } from '@repo/ui/components/ui/button';

export function CreatePostForm() {
  return (
    <form action={createPost}>
      <input name="title" required />
      <textarea name="content" required />
      <Button type="submit">Create Post</Button>
    </form>
  );
}

Benefits:

  • ✅ Type-safe mutations
  • ✅ No API routes needed
  • ✅ Progressive enhancement
  • ✅ Automatic revalidation

Use When:

  • Creating, updating, deleting data
  • Form submissions
  • User actions

API Route Data Flow (External)

Used for webhooks, callbacks, and external integrations.

Example

// app/api/payment/vipps/callback/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createAdminClient } from '@repo/api/server';
import { handleVippsCallback } from '@repo/payment/vipps';

export async function POST(request: NextRequest) {
  // Verify Vipps signature
  const authToken = request.headers.get('authorization');
  if (!verifyVippsSignature(authToken)) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }
  
  // Process webhook
  const sessionId = request.nextUrl.searchParams.get('sessionId');
  const { db } = await createAdminClient(); // Admin client (no user session)
  
  await handleVippsCallback(authToken, sessionId, db);
  
  return NextResponse.json({ success: true });
}

Benefits:

  • ✅ Handle external events
  • ✅ Server-side validation
  • ✅ No user session needed

Use When:

  • Payment webhooks
  • Third-party callbacks
  • Scheduled tasks
  • External integrations

Client-Side Data Flow

Sometimes data needs to be fetched on the client.

Example

'use client';

import { useEffect, useState } from 'react';

export function LiveData() {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    fetch('/api/live-data')
      .then(res => res.json())
      .then(setData);
  }, []);
  
  if (!data) return <div>Loading...</div>;
  
  return <div>{/* Render data */}</div>;
}

Use When:

  • Real-time data
  • User-specific dynamic data
  • Polling/live updates
  • Data that can't be pre-rendered
⚠️
Prefer Server Components

Only use client-side fetching when necessary. Server Components are usually better for performance and SEO.


Payment Flow (Detailed)

A complete payment flow showing all data movement:

Race Condition Handling

The webhook and return redirect can arrive in any order. The system handles this:

export async function verifyOrderStatus(orderId: string, db: Database) {
  // Get current order
  const order = await db.getDocument('orders', orderId);
  
  // If already processed by webhook, skip Vipps check
  if (order.status !== 'pending') {
    return order;
  }
  
  // Otherwise, check with Vipps and update
  const vippsStatus = await getVippsSessionStatus(order.vippsSessionId);
  await db.updateDocument('orders', orderId, {
    status: vippsStatus.status,
    updatedAt: new Date().toISOString(),
  });
  
  return order;
}

Authentication Flow

ℹ️
Authentication Methods

BISO Sites uses Magic Link (email link) for web app and OAuth (Microsoft) for admin app. No password authentication is used.

OAuth Flow (Admin App)

Session Management

// Magic Link: Send link (web app)
const { account } = await createAdminClient();
await account.createMagicURLToken(ID.unique(), email, callbackUrl);

// OAuth: Initiate flow (admin app)
const { account } = await createAdminClient();
const redirectUrl = await account.createOAuth2Token(
  OAuthProvider.Microsoft,
  successUrl,
  failureUrl
);

// Handle callback: Create session (both apps)
const { account } = await createAdminClient();
const session = await account.createSession(userId, secret);
cookies().set("a_session_biso", session.secret, { httpOnly: true });

// Verify session (middleware)
const { account } = await createSessionClient();
try {
  const user = await account.get();
  // User is authenticated
} catch {
  // No valid session
  redirect('/login');
}

// Delete session (logout)
const { account } = await createSessionClient();
await account.deleteSession('current');

Cache & Revalidation

Automatic Caching

Next.js caches Server Component data by default:

// This is cached automatically
const posts = await db.listDocuments('posts');

Manual Revalidation

import { revalidatePath, revalidateTag } from 'next/cache';

// Revalidate specific path
revalidatePath('/posts');

// Revalidate all pages with tag
revalidateTag('posts');

Opt-out of Caching

// Dynamic (no cache)
export const dynamic = 'force-dynamic';

// Time-based revalidation
export const revalidate = 60; // 60 seconds

Real-time Updates

For real-time features, Appwrite provides subscriptions:

'use client';

import { Client, Databases } from '@repo/api/client';

export function LivePosts() {
  useEffect(() => {
    const client = new Client()
      .setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT!)
      .setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID!);
      
    const databases = new Databases(client);
    
    // Subscribe to changes
    const unsubscribe = databases.subscribe(
      'database_id.posts_collection',
      (response) => {
        console.log('Post updated:', response.payload);
        // Update UI
      }
    );
    
    return () => unsubscribe();
  }, []);
  
  // ...
}

File Upload Flow

Example

'use server';

import { createSessionClient } from '@repo/api/server';
import { ID } from 'node-appwrite';

export async function uploadFile(formData: FormData) {
  const file = formData.get('file') as File;
  
  const { storage } = await createSessionClient();
  const uploaded = await storage.createFile(
    'bucket_id',
    ID.unique(),
    file
  );
  
  return {
    fileId: uploaded.$id,
    url: getStorageFileUrl('bucket_id', uploaded.$id),
  };
}

Best Practices

Prefer Server Components

// ✅ Good: Server Component
async function PostsList() {
  const posts = await getPosts(); // Direct DB access
  return <div>{posts.map(...)}</div>;
}

// ❌ Avoid: Client Component with fetch
'use client';
function PostsList() {
  const [posts, setPosts] = useState([]);
  useEffect(() => {
    fetch('/api/posts').then(...);
  }, []);
  return <div>{posts.map(...)}</div>;
}

Use Server Actions for Mutations

// ✅ Good: Server Action
'use server';
async function deletePost(postId: string) {
  const { db } = await createSessionClient();
  await db.deleteDocument('posts', postId);
  revalidatePath('/posts');
}

// ❌ Avoid: API Route for simple mutations
// app/api/posts/[id]/route.ts
export async function DELETE(request: NextRequest, { params }) {
  // Unnecessary API route
}

Revalidate After Mutations

'use server';

export async function createPost(data) {
  await db.createDocument(...);
  
  // ✅ Revalidate to show new data
  revalidatePath('/posts');
  revalidatePath('/');
}

Handle Errors Gracefully

'use server';

export async function createPost(data) {
  try {
    const post = await db.createDocument(...);
    revalidatePath('/posts');
    return { success: true, post };
  } catch (error) {
    console.error('Failed to create post:', error);
    return { success: false, error: 'Failed to create post' };
  }
}

Use Admin Client for Webhooks

// Webhooks don't have user sessions
// Use admin client instead

export async function POST(request: NextRequest) {
  // ✅ Good: Admin client
  const { db } = await createAdminClient();
  
  // ❌ Wrong: Session client (no session exists)
  // const { db } = await createSessionClient();
}

Common Patterns

Loading States

// Use Suspense for Server Components
<Suspense fallback={<Loading />}>
  <PostsList />
</Suspense>

Error Handling

// Error boundaries
export default function Error({ error, reset }) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button onClick={reset}>Try again</button>
    </div>
  );
}

Optimistic Updates

'use client';

import { useOptimistic } from 'react';

export function TodoList({ todos }) {
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (state, newTodo) => [...state, newTodo]
  );
  
  async function createTodo(formData) {
    const title = formData.get('title');
    addOptimisticTodo({ title, id: 'temp' }); // Show immediately
    await createTodoAction(formData); // Save to server
  }
  
  return (
    <form action={createTodo}>
      {/* ... */}
    </form>
  );
}

Next Steps

ℹ️
Related Documentation