Skip to content

Cloud Functions

The cloud functions are serverless edge functions deployed on Cloudflare Workers, handling specific tasks like file processing and JWT operations.

Technology Stack

  • Cloudflare Workers - Serverless edge computing platform
  • Hono - Lightweight web framework
  • Wrangler - Cloudflare Workers CLI tool
  • TypeScript - Type-safe JavaScript

Project Structure

apps/cloud-functions/
├── src/
│   ├── functions/         # Individual cloud functions
│   │   ├── analytics.ts
│   │   ├── fileProcessor.ts
│   │   └── ...
│   │
│   ├── utils/            # Utility functions
│   │   └── ...
│   │
│   ├── middleware.ts     # Hono middleware
│   ├── router.ts         # Route definitions
│   ├── jwks.ts           # JWT key set handling
│   ├── types.ts          # Type definitions
│   └── index.ts          # Entry point

├── wrangler.toml         # Cloudflare Workers configuration
├── tsconfig.json         # TypeScript configuration
└── package.json

Architecture

Cloud Functions run at the edge on Cloudflare's global network, providing low-latency access to specific operations.

Entry Point

typescript
// src/index.ts
import { Hono } from "hono";
import { cors } from "hono/cors";
import { router } from "./router";
import type { Env } from "./types";

const app = new Hono<{ Bindings: Env }>();

// Middleware
app.use(
  "*",
  cors({
    origin: (origin) => origin,
    allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
    allowHeaders: ["Content-Type", "Authorization"],
    credentials: true,
  }),
);

// Mount routes
app.route("/", router);

// Health check
app.get("/health", (c) => c.json({ status: "ok" }));

export default app;

Router Configuration

typescript
// src/router.ts
import { Hono } from "hono";
import { authMiddleware } from "./middleware";
import { fileProcessor } from "./functions/fileProcessor";
import { jwtUtil } from "./functions/jwtUtil";
import type { Env } from "./types";

export const router = new Hono<{ Bindings: Env }>();

// Public endpoints
router.get("/.well-known/jwks.json", jwtUtil.getJWKS);

// Protected endpoints
router.use("/api/*", authMiddleware);
router.post("/api/process-file", fileProcessor.processFile);
router.post("/api/generate-jwt", jwtUtil.generateToken);
router.post("/api/verify-jwt", jwtUtil.verifyToken);

Middleware

Authentication Middleware

typescript
// src/middleware.ts
import { Context, Next } from "hono";
import { verify } from "hono/jwt";
import type { Env } from "./types";

export async function authMiddleware(
  c: Context<{ Bindings: Env }>,
  next: Next,
) {
  const authHeader = c.req.header("Authorization");

  if (!authHeader?.startsWith("Bearer ")) {
    return c.json({ error: "Unauthorized" }, 401);
  }

  const token = authHeader.substring(7);

  try {
    const payload = await verify(token, c.env.JWT_SECRET);
    c.set("user", payload);
    await next();
  } catch (error) {
    return c.json({ error: "Invalid token" }, 401);
  }
}

export async function corsMiddleware(
  c: Context<{ Bindings: Env }>,
  next: Next,
) {
  await next();

  c.header("Access-Control-Allow-Origin", "*");
  c.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
  c.header("Access-Control-Allow-Headers", "Content-Type, Authorization");
}

Functions

File Processing

typescript
// src/functions/fileProcessor.ts
import { Context } from "hono";
import AdmZip from "adm-zip";
import type { Env } from "../types";

export const fileProcessor = {
  async processFile(c: Context<{ Bindings: Env }>) {
    const body = await c.req.parseBody();
    const file = body.file as File;

    if (!file) {
      return c.json({ error: "No file provided" }, 400);
    }

    try {
      // Example: Extract zip file
      const buffer = await file.arrayBuffer();
      const zip = new AdmZip(Buffer.from(buffer));
      const entries = zip.getEntries();

      const files = entries.map((entry) => ({
        name: entry.entryName,
        size: entry.header.size,
        isDirectory: entry.isDirectory,
      }));

      return c.json({
        success: true,
        fileCount: files.length,
        files,
      });
    } catch (error) {
      return c.json(
        {
          error: "Failed to process file",
          details: error.message,
        },
        500,
      );
    }
  },
};

JWT Utilities

typescript
// src/functions/jwtUtil.ts
import { Context } from "hono";
import { sign, verify } from "hono/jwt";
import type { Env } from "../types";
import { getJWKS } from "../jwks";

export const jwtUtil = {
  async generateToken(c: Context<{ Bindings: Env }>) {
    const body = await c.req.json();
    const { userId, email, role } = body;

    if (!userId || !email) {
      return c.json({ error: "Missing required fields" }, 400);
    }

    const payload = {
      sub: userId,
      email,
      role,
      iat: Math.floor(Date.now() / 1000),
      exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24, // 24 hours
    };

    const token = await sign(payload, c.env.JWT_SECRET);

    return c.json({ token });
  },

  async verifyToken(c: Context<{ Bindings: Env }>) {
    const body = await c.req.json();
    const { token } = body;

    if (!token) {
      return c.json({ error: "Token required" }, 400);
    }

    try {
      const payload = await verify(token, c.env.JWT_SECRET);
      return c.json({ valid: true, payload });
    } catch (error) {
      return c.json({ valid: false, error: error.message }, 401);
    }
  },

  async getJWKS(c: Context<{ Bindings: Env }>) {
    // Return JSON Web Key Set for token verification
    const jwks = getJWKS(c.env.JWT_SECRET);
    return c.json(jwks);
  },
};

JWKS Implementation

typescript
// src/jwks.ts
import { createPublicKey } from "crypto";

export function getJWKS(secret: string) {
  // Generate JWKS from secret
  // This is a simplified example - in production, use proper key management

  return {
    keys: [
      {
        kty: "oct",
        kid: "default",
        use: "sig",
        alg: "HS256",
        // In production, don't expose the secret!
        // Use proper public/private key pairs
      },
    ],
  };
}

Environment Variables

Environment variables are configured in Wrangler:

toml
# wrangler.toml
name = "lms-cloud-functions"
main = "src/index.ts"
compatibility_date = "2024-01-01"

[vars]
# Non-sensitive variables

[env.production]
vars = { ENVIRONMENT = "production" }

[env.development]
vars = { ENVIRONMENT = "development" }

Secret variables are stored separately:

bash
# Set secrets using Wrangler
wrangler secret put JWT_SECRET
wrangler secret put API_KEY

Local development (.dev.vars):

env
JWT_SECRET=your_local_jwt_secret
API_KEY=your_local_api_key

Type Definitions

typescript
// src/types.ts
export interface Env {
  // Environment variables
  JWT_SECRET: string;
  ENVIRONMENT: "development" | "production";

  // KV namespaces (if using)
  CACHE: KVNamespace;

  // Durable Objects (if using)
  // COUNTER: DurableObjectNamespace;
}

export interface User {
  id: number;
  email: string;
  role: string;
}

export interface JWTPayload {
  sub: number;
  email: string;
  role: string;
  iat: number;
  exp: number;
}

Development

Local Development

bash
# Start development server
pnpm dev

# This runs:
wrangler dev

The dev server runs at http://localhost:8787 by default.

Testing Locally

bash
# Test health check
curl http://localhost:8787/health

# Test JWT generation
curl -X POST http://localhost:8787/api/generate-jwt \
  -H "Content-Type: application/json" \
  -d '{"userId": 1, "email": "test@example.com", "role": "student"}'

# Test JWT verification
curl -X POST http://localhost:8787/api/verify-jwt \
  -H "Content-Type: application/json" \
  -d '{"token": "your.jwt.token"}'

Deployment

Deploy to Cloudflare

bash
# Deploy to production
pnpm deploy

# This runs:
wrangler deploy

# Deploy to staging
wrangler deploy --env staging

Deployment Configuration

toml
# wrangler.toml
[env.production]
name = "lms-cloud-functions"
routes = [
  { pattern = "functions.example.com/*", zone_name = "example.com" }
]

[env.staging]
name = "lms-cloud-functions-staging"
routes = [
  { pattern = "functions-staging.example.com/*", zone_name = "example.com" }
]

Performance Considerations

Edge Caching

typescript
// Cache responses at the edge
app.get("/api/data", async (c) => {
  const cacheKey = new Request(c.req.url, c.req.raw);
  const cache = caches.default;

  // Check cache
  let response = await cache.match(cacheKey);

  if (!response) {
    // Fetch data
    const data = await fetchData();
    response = new Response(JSON.stringify(data), {
      headers: {
        "Content-Type": "application/json",
        "Cache-Control": "public, max-age=300", // 5 minutes
      },
    });

    // Store in cache
    await cache.put(cacheKey, response.clone());
  }

  return response;
});

KV Storage

For persistent storage across edge locations:

typescript
// Store data in KV
await c.env.CACHE.put("key", JSON.stringify(data), {
  expirationTtl: 3600, // 1 hour
});

// Retrieve from KV
const cached = await c.env.CACHE.get("key", "json");

Error Handling

typescript
// Global error handler
app.onError((err, c) => {
  console.error("Error:", err);

  return c.json(
    {
      error: "Internal server error",
      message: err.message,
      timestamp: new Date().toISOString(),
    },
    500,
  );
});

// Not found handler
app.notFound((c) => {
  return c.json(
    {
      error: "Not found",
      path: c.req.path,
    },
    404,
  );
});

Monitoring

Logging

typescript
// Structured logging
app.use("*", async (c, next) => {
  const start = Date.now();
  await next();
  const duration = Date.now() - start;

  console.log(
    JSON.stringify({
      method: c.req.method,
      path: c.req.path,
      status: c.res.status,
      duration,
      timestamp: new Date().toISOString(),
    }),
  );
});

Analytics

Cloudflare Workers Analytics provides:

  • Request count
  • Error rate
  • CPU time
  • Duration percentiles

Access via Cloudflare Dashboard or Workers Analytics API.

Security

Rate Limiting

typescript
// Simple rate limiting
const rateLimiter = new Map<string, number[]>();

async function rateLimit(c: Context, limit: number = 10): Promise<boolean> {
  const ip = c.req.header("CF-Connecting-IP") || "unknown";
  const now = Date.now();
  const windowMs = 60000; // 1 minute

  const requests = rateLimiter.get(ip) || [];
  const recentRequests = requests.filter((time) => now - time < windowMs);

  if (recentRequests.length >= limit) {
    return false;
  }

  recentRequests.push(now);
  rateLimiter.set(ip, recentRequests);

  return true;
}

// Use in routes
app.post("/api/endpoint", async (c) => {
  if (!(await rateLimit(c, 10))) {
    return c.json({ error: "Rate limit exceeded" }, 429);
  }

  // Process request
});

Input Validation

typescript
import { z } from "zod";

const schema = z.object({
  userId: z.number().positive(),
  email: z.string().email(),
  role: z.enum(["student", "instructor", "admin"]),
});

app.post("/api/create-user", async (c) => {
  const body = await c.req.json();

  try {
    const validated = schema.parse(body);
    // Process validated data
  } catch (error) {
    return c.json({ error: "Validation failed", details: error.errors }, 400);
  }
});

Best Practices

  1. Keep functions small - Edge functions have execution time limits
  2. Use KV for persistence - Worker memory is ephemeral
  3. Implement caching - Leverage edge caching for performance
  4. Handle errors gracefully - Return meaningful error responses
  5. Monitor performance - Use Cloudflare Analytics
  6. Secure endpoints - Implement authentication and rate limiting
  7. Type everything - Use TypeScript for safety
  8. Test locally - Use wrangler dev before deploying

Limitations

  • CPU time: 50ms (free), 50ms-30s (paid)
  • Memory: 128 MB
  • Request size: 100 MB
  • Response size: Unlimited
  • Execution time: Max 30 seconds (paid plan)

Integration with Backend

The backend can call cloud functions:

typescript
// apps/client-backend/src/gateways/cloudFunctions.gateway.ts
@singleton()
export class CloudFunctionsGateway {
  private baseUrl = process.env.CLOUD_FUNCTIONS_URL;

  async processFile(file: Buffer): Promise<ProcessResult> {
    const formData = new FormData();
    formData.append("file", new Blob([file]));

    const response = await fetch(`${this.baseUrl}/api/process-file`, {
      method: "POST",
      body: formData,
      headers: {
        Authorization: `Bearer ${this.getServiceToken()}`,
      },
    });

    return response.json();
  }

  private getServiceToken(): string {
    // Generate service-to-service token
    return jwt.sign({ service: "backend" }, process.env.JWT_SECRET);
  }
}