Sketch
SketchDocs
by FileKit Studio
Whiteboard SDK

Diagram Sharing & Encryption

Zero-knowledge link sharing, client-side AES-128-GCM encryption, snapshot isolation, deduplication, and rate limiting.

FileKit Sketch includes an end-to-end encrypted sharing engine. Anyone can share a point-in-time snapshot of their whiteboard using a single URL without creating an account or exposing unencrypted diagrams to any server.


1. Zero-Knowledge Security Architecture

Diagram sharing in FileKit Sketch is 100% Zero-Knowledge:

Zero-Knowledge End-to-End Encryption Architecture
Loading diagram canvas...
Pan & zoom enabled • 1-click PNG exportPowered by @filekit-studio/diagram
  1. In-Browser Encryption:
    • The browser generates a cryptographically random 128-bit symmetric key (AES-128-GCM) and a 12-byte IV using the Web Crypto API.
    • The drawing payload is compressed with native CompressionStream('deflate-raw') and encrypted locally.
  2. Key Stays in # Hash Fragment:
    • The generated link has the format https://sketch.../playground#json=<id>,<key>.
    • RFC Standard: By browser HTTP specification, URL fragments following # are never transmitted over the network or logged in server access logs.
  3. Server Sees Only Scrambled Noise:
    • Neither our servers nor Upstash Redis ever have access to the decryption key. Only users who possess the complete link can decrypt and view the canvas snapshot.

2. Snapshot Isolation & Local Edits

A shared link represents an immutable point-in-time snapshot taken at the exact moment the Share button was clicked.

Edits never overwrite the link: If someone opens a shared link and draws or edits the canvas, their changes are saved only to their local browser's localStorage. The original shared link remains completely unchanged.

To publish updates or revisions, simply click Share again to generate a new snapshot link.


3. Dual-Routing Architecture

The engine automatically selects the optimal link strategy based on diagram complexity:

Dual-Routing Architecture & Fallback Strategy
Loading diagram canvas...
Pan & zoom enabled • 1-click PNG exportPowered by @filekit-studio/diagram
Diagram SizeRouting ModeURL FormatServer StorageLink Lifespan
Small / Medium
(≤\le 1,800 chars)
Pure Client Deflate#s=<base64url>0 Server Calls (100% Offline)Permanent
Large / Complex
(50 – 500+ shapes)
Encrypted Cloud Shortlink#json=<id>,<key>Upstash Redis (Ciphertext only)7-Day Retention

If Upstash is unreachable or the user is rate-limited, the system automatically falls back to the #s= client link so sharing never fails.


4. Intelligent Content Fingerprinting (Deduplication)

Re-clicking the Share button on an unchanged diagram will not spam Redis or consume user storage quotas:

Content Fingerprinting & Zero-Network Cache Lifecycle
Loading diagram canvas...
Pan & zoom enabled • 1-click PNG exportPowered by @filekit-studio/diagram
  1. Transient UI Stripping:
    • Selecting a shape, moving the cursor, or zooming does not alter the drawing data. The engine strips isSelected, isHovered, and isEditing flags before hashing.
  2. Structural Fingerprint:
    • A fingerprint of the sanitized elements is compared against the last generated share result.
  3. Instant Cache Return:
    • If no structural changes (shapes, positions, text, colors) occurred, the modal returns the existing link in 0ms without initiating any network calls.

5. Opening a Shared Diagram

When a user visits a shared URL in another browser, Sketch detects the incoming snapshot and prompts them with a confirmation dialog:

Shared Snapshot Resolution Decision Flow
Loading diagram canvas...
Pan & zoom enabled • 1-click PNG exportPowered by @filekit-studio/diagram
  • Replace Current Canvas: Replaces the canvas with the shared snapshot, updates localStorage, and triggers zoomToFit(). The previous drawing can be restored anytime with Undo (Ctrl + Z).
  • Insert Below Existing: Keeps the user's active drawing untouched and appends the shared snapshot 80px below the existing elements.
  • Empty Canvas: If the board has 0 elements, the shared snapshot loads directly without prompting.

6. Abuse Prevention & Quotas

To protect infrastructure from bots, scrapers, and denial-of-service attempts:

  • Payload Size Ceiling: Requests exceeding 500 KB are rejected before processing.
  • Tier 1 Anti-DDoS Rate Limit: @upstash/ratelimit enforces a maximum of 5 share requests per 10 seconds per salted IP hash (SHA-256(ip:salt)).
  • Tier 2 User Quota (FIFO): An atomic Redis Lua script caps active links to 5 links per hour per user. Generating a 6th link automatically evicts the oldest entry (LPOP + DEL).
  • Graceful Fallback: If rate limits are reached, the UI displays a subtle cooldown notice and switches seamlessly to generating a self-contained #s= client link.

7. Implementing Sharing in Your Own App

The @filekit-studio/sketch npm package is 100% client-side and unopinionated. It does not bundle proprietary database connections or vendor locks. You have complete freedom over how diagrams are shared in your application.

The <SketchEditor /> Sharing Contract

<SketchEditor /> provides three built-in touchpoints for sharing:

  1. onShare?: () => void: Fired when the user clicks the Share button in the top bar. Simply passing this callback automatically mounts the Share button.
  2. uiConfig={{ showShareButton: true }}: Explicit toggle for the Share button.
    • Default: false when onShare is omitted; true automatically whenever onShare is passed.
    • Strict Invariant: If you explicitly set showShareButton: true, the onShare callback is required. If omitted, a console warning is logged and clicking the button will throw a runtime error.
  3. controller.importElements(elements, { sourceType: 'share' }): Prompts the user with the built-in smart confirmation modal ("Replace Current Canvas" vs "Insert Below Existing") and automatically fits the camera with zoomToFit().
TypeScript (TSX)
<SketchEditor
  controllerRef={controllerRef}
  onShare={handleShare} // Automatically mounts the Share button in the top bar
/>

Option A: Pure Client-Side URL Hash Sharing (Zero Backend, 100% Free)

This is the fastest way to add sharing. It compresses the canvas drawing directly into the browser URL hash using native Web APIs (CompressionStream and DecompressionStream).

  • Zero backend required: No servers, databases, or API keys.
  • Works offline: Ideal for static sites, documentation playgrounds, and internal tools.
  • Instant: 0ms latency with permanent link longevity.
components/whiteboard-with-share.tsx
"use client";

import React, { useRef, useEffect } from "react";
import dynamic from "next/dynamic";
import type { SketchController, SketchBoardElement } from "@filekit-studio/sketch";

const SketchEditor = dynamic(
  () => import("@filekit-studio/sketch").then((mod) => mod.SketchEditor),
  { ssr: false }
);

// 1. Native browser Deflate-Raw compression to URL-safe Base64
async function compressToHash(elements: SketchBoardElement[]): Promise<string> {
  const json = JSON.stringify(elements);
  const stream = new Blob([new TextEncoder().encode(json)])
    .stream()
    .pipeThrough(new CompressionStream("deflate-raw"));
  const buffer = await new Response(stream).arrayBuffer();
  const bytes = new Uint8Array(buffer);
  
  let binary = "";
  for (let i = 0; i < bytes.length; i++) {
    binary += String.fromCharCode(bytes[i]);
  }
  return btoa(binary)
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=+$/, "");
}

// 2. Native browser DecompressionStream from URL-safe Base64
async function decompressFromHash(base64Url: string): Promise<SketchBoardElement[]> {
  const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
  const padLen = (4 - (base64.length % 4)) % 4;
  const padded = base64 + "=".repeat(padLen);
  const binary = atob(padded);
  
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }
  
  const stream = new Blob([bytes])
    .stream()
    .pipeThrough(new DecompressionStream("deflate-raw"));
  const json = await new Response(stream).text();
  return JSON.parse(json) as SketchBoardElement[];
}

export function WhiteboardWithShare() {
  const controllerRef = useRef<SketchController | null>(null);

  // 3. User clicks the top-bar Share button
  const handleShare = async () => {
    if (!controllerRef.current) return;
    const elements = controllerRef.current.getElements();
    if (!elements || elements.length === 0) {
      controllerRef.current.showToast?.("Canvas is empty. Draw something before sharing!");
      return;
    }

    try {
      const hash = await compressToHash(elements);
      const shareUrl = `${window.location.origin}${window.location.pathname}#s=${hash}`;
      await navigator.clipboard.writeText(shareUrl);
      controllerRef.current.showToast?.("Share link copied to clipboard!");
    } catch (err) {
      console.error("Failed to generate share link:", err);
    }
  };

  // 4. Automatically detect and load shared diagrams when visiting the link
  useEffect(() => {
    const hash = window.location.hash;
    if (!hash.startsWith("#s=")) return;

    const payload = hash.slice(3);
    if (!payload) return;

    decompressFromHash(payload)
      .then((elements) => {
        // Automatically triggers the smart confirmation modal ("Replace" vs "Insert Below")
        controllerRef.current?.importElements?.(elements, { sourceType: "share" });
      })
      .catch((err) => {
        console.error("Failed to decompress shared diagram:", err);
      });
  }, []);

  return (
    <div className="w-full h-screen">
      <SketchEditor
        controllerRef={controllerRef}
        onShare={handleShare}
        uiConfig={{ showShareButton: true }}
      />
    </div>
  );
}

If you want clean, compact URLs (e.g., https://yourapp.com/board/d_a9f1b2) or need to support complex whiteboards with hundreds of detailed elements, persist the snapshot in your own backend (PostgreSQL, Supabase, Upstash Redis, AWS S3, or MongoDB).

1. Backend Route Handler (Next.js App Router)

Create an API endpoint to store and retrieve snapshots:

app/api/share/route.ts
import { NextRequest, NextResponse } from "next/server";
// Replace with your ORM / client: Prisma, Drizzle, Supabase, or Redis
import { redis } from "@/lib/redis";
import { nanoid } from "nanoid";

// POST /api/share - Save snapshot
export async function POST(req: NextRequest) {
  try {
    const { elements } = await req.json();
    if (!elements || !Array.isArray(elements)) {
      return NextResponse.json({ error: "Invalid diagram payload" }, { status: 400 });
    }

    const id = nanoid(10);
    // Persist with a 30-day expiration (or permanent)
    await redis.set(`share:${id}`, JSON.stringify(elements), { ex: 60 * 60 * 24 * 30 });

    return NextResponse.json({ id });
  } catch (err) {
    return NextResponse.json({ error: "Failed to store snapshot" }, { status: 500 });
  }
}

// GET /api/share?id=abc123xyz - Retrieve snapshot
export async function GET(req: NextRequest) {
  const { searchParams } = new URL(req.url);
  const id = searchParams.get("id");
  if (!id) {
    return NextResponse.json({ error: "Missing snapshot ID" }, { status: 400 });
  }

  const data = await redis.get(`share:${id}`);
  if (!data) {
    return NextResponse.json({ error: "Diagram not found or expired" }, { status: 404 });
  }

  const elements = typeof data === "string" ? JSON.parse(data) : data;
  return NextResponse.json({ elements });
}

2. Frontend React Integration

Wire onShare to post the diagram and load it back on mount:

components/cloud-whiteboard.tsx
"use client";

import React, { useRef, useEffect } from "react";
import dynamic from "next/dynamic";
import type { SketchController } from "@filekit-studio/sketch";

const SketchEditor = dynamic(
  () => import("@filekit-studio/sketch").then((mod) => mod.SketchEditor),
  { ssr: false }
);

interface CloudWhiteboardProps {
  shareId?: string; // Optional route param, e.g. /board/[id]
}

export function CloudWhiteboard({ shareId }: CloudWhiteboardProps) {
  const controllerRef = useRef<SketchController | null>(null);

  // 1. Share button handler: post to your API
  const handleShare = async () => {
    if (!controllerRef.current) return;
    const elements = controllerRef.current.getElements();

    try {
      const res = await fetch("/api/share", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ elements }),
      });

      if (!res.ok) throw new Error("Share request failed");
      const { id } = (await res.json()) as { id: string };

      const shareUrl = `${window.location.origin}/board/${id}`;
      await navigator.clipboard.writeText(shareUrl);
      controllerRef.current.showToast?.("Cloud share link copied to clipboard!");
    } catch (err) {
      console.error("Failed to share diagram:", err);
    }
  };

  // 2. Load snapshot if opened via /board/[id]
  useEffect(() => {
    if (!shareId) return;

    fetch(`/api/share?id=${encodeURIComponent(shareId)}`)
      .then((res) => {
        if (!res.ok) throw new Error("Snapshot not found");
        return res.json();
      })
      .then((data: { elements: unknown }) => {
        if (Array.isArray(data.elements)) {
          controllerRef.current?.importElements?.(data.elements, { sourceType: "share" });
        }
      })
      .catch((err) => console.error("Error loading snapshot:", err));
  }, [shareId]);

  return (
    <div className="w-full h-screen">
      <SketchEditor
        controllerRef={controllerRef}
        onShare={handleShare}
        uiConfig={{ showShareButton: true }}
      />
    </div>
  );
}

Option C: Adding Zero-Knowledge Encryption (Enterprise & Privacy)

If your app requires zero-knowledge confidentiality (where even your database administrators cannot view diagrams):

  1. Encrypt on Client: Use crypto.subtle.generateKey and crypto.subtle.encrypt with AES-128-GCM before sending bytes to /api/share.
  2. Store Only Ciphertext: Send the encrypted ciphertext envelope to your backend.
  3. Decryption Key Stays in Hash: Place the symmetric key strictly after the # fragment in the URL (e.g., /board/ID#KEY).
  4. RFC Invariant: URL fragments after # are strictly client-side and never reach your web server or get logged in access logs, ensuring 100% zero-knowledge security.

On this page