> For the complete documentation index, see [llms.txt](https://docs.getlimy.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.getlimy.ai/limy-pixel/cdn-integration/netlify-free-plan.md).

# Netlify (Free Plan)

Send access logs from any Netlify site to an HTTP endpoint using Edge Functions, Netlify Blobs, and a scheduled function. Logs are batched and sent every minute.

Works on **all Netlify plans** including the free tier.

Outbound requests to your log endpoint are authenticated via an `x-api-key` header. The key is stored as a Netlify secret env var (`LOG_API_KEY`) and never shipped to the browser — client logs go through a same-origin proxy function that injects the key server-side.

### Architecture

```
Request → Edge Function → Netlify Blobs (buffer)
                              ↓
         Scheduled Function (every 1 min) → batch POST (x-api-key) → your endpoint
                              ↓
         Cleanup: deletes sent entries from Blobs

Client SPA navigation → useAccessLog hook → /api/access-log proxy fn (adds x-api-key) → your endpoint
```

### Setup

#### 1. Create the Edge Function

Create `netlify/edge-functions/access-log.ts`:

```typescript
import { getStore } from "@netlify/blobs";
import type { Config, Context } from "@netlify/edge-functions";

export default async (request: Request, context: Context) => {
  const startTime = Date.now();
  const response = await context.next();
  const duration = Date.now() - startTime;
  const url = new URL(request.url);

  const logEntry = {
    timestamp: new Date().toISOString(),
    method: request.method,
    url: request.url,
    path: url.pathname,
    query: url.search,
    status_code: response.status,
    duration_ms: duration,
    client_ip: context.ip,
    user_agent: request.headers.get("user-agent"),
    referer: request.headers.get("referer"),
    content_type: response.headers.get("content-type"),
    country: context.geo.country?.code,
    city: context.geo.city,
    region: context.server.region,
    request_id: context.requestId,
    site: context.site.name,
    deploy_id: context.deploy.id,
  };

  const key = `${Date.now()}-${crypto.randomUUID()}`;

  context.waitUntil(
    getStore("access-logs")
      .setJSON(key, logEntry)
      .catch((err) => console.error("Failed to buffer access log:", err))
  );

  return response;
};

export const config: Config = {
  path: "/*",
};
```

> To exclude static assets, add `excludedPath` to the config:
>
> ```typescript
> export const config: Config = {
>   path: "/*",
>   excludedPath: ["/*.css", "/*.js", "/*.png", "/*.jpg", "/*.svg", "/*.woff2", "/*.ico"],
> };
> ```

#### 2. Create the Scheduled Flush Function

Create `netlify/functions/flush-access-logs.mts`:

```typescript
import { getStore } from "@netlify/blobs";
import type { Config } from "@netlify/functions";

const LOG_ENDPOINT = "https://stream.getlimy.ai";

export default async () => {
  const store = getStore("access-logs");
  const { blobs } = await store.list();

  if (blobs.length === 0) return;

  const entries = await Promise.all(
    blobs.map(async (blob) => {
      const data = await store.get(blob.key, { type: "json" });
      return { key: blob.key, data };
    })
  );

  const logEntries = entries
    .filter((e) => e.data !== null)
    .map((e) => e.data);

  if (logEntries.length === 0) return;

  const apiKey = process.env.LOG_API_KEY;
  if (!apiKey) {
    console.error("LOG_API_KEY env var not set; skipping flush");
    return;
  }

  const response = await fetch(LOG_ENDPOINT, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "User-Agent": "LimyAnalyticsNetlifyAccessLogs",
      "x-api-key": apiKey,
    },
    body: JSON.stringify(logEntries),
  });

  if (!response.ok) {
    console.error(`Failed to flush logs: ${response.status} ${response.statusText}`);
    return;
  }

  // Only delete after successful send
  await Promise.all(blobs.map((blob) => store.delete(blob.key)));

  console.log(`Flushed ${logEntries.length} access log entries`);
};

export const config: Config = {
  schedule: "* * * * *", // Every minute
};
```

#### 3. Create the Client Log Proxy Function

The browser cannot ship the API key (it would leak in the bundle) and `navigator.sendBeacon` cannot set custom headers. Solution: a same-origin proxy function that adds `x-api-key` server-side.

Create `netlify/functions/log-proxy.mts`:

```typescript
import type { Context } from "@netlify/functions";

const LOG_ENDPOINT = "https://stream.getlimy.ai";

export default async (request: Request, _context: Context) => {
  if (request.method !== "POST") {
    return new Response("Method Not Allowed", { status: 405 });
  }

  const apiKey = process.env.LOG_API_KEY;
  if (!apiKey) {
    console.error("LOG_API_KEY env var not set");
    return new Response("Server misconfigured", { status: 500 });
  }

  const body = await request.text();

  const response = await fetch(LOG_ENDPOINT, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "User-Agent": "LimyAnalyticsNetlifyAccessLogs",
      "x-api-key": apiKey,
    },
    body,
  });

  if (!response.ok) {
    console.error(`Log forward failed: ${response.status} ${response.statusText}`);
    return new Response("Upstream error", { status: 502 });
  }

  return new Response(null, { status: 204 });
};

export const config = {
  path: "/api/access-log",
};
```

#### 4. Update `netlify.toml`

Add the edge function declaration to your `netlify.toml`:

```toml
[[edge_functions]]
  function = "access-log"
  path = "/*"
```

#### 5. Install Dependencies

```bash
npm install @netlify/blobs @netlify/functions
```

#### 6. Configure the API Key Secret

Set `LOG_API_KEY` as a Netlify secret env var. Get your API Key from your Limy dashboard

**CLI:**

```bash
netlify env:set LOG_API_KEY "your-secret-key" --secret
```

#### 7. Deploy

Push to your connected git repo. Netlify deploys automatically. Make sure `LOG_API_KEY` is set in the target deploy context (production / deploy previews / branch).

### Configuration

| Setting                 | Where                                          | Default                          |
| ----------------------- | ---------------------------------------------- | -------------------------------- |
| Log endpoint URL        | `flush-access-logs.mts` + `log-proxy.mts`      | — (required)                     |
| API key                 | Netlify env var `LOG_API_KEY` (mark as secret) | — (required)                     |
| Flush interval (server) | `flush-access-logs.mts` → `config.schedule`    | Every minute (`* * * * *`)       |
| Flush interval (client) | `useAccessLog.ts` → `FLUSH_INTERVAL_MS`        | 60,000 ms                        |
| Client batch size       | `useAccessLog.ts` → `BATCH_SIZE`               | 100 events                       |
| Excluded paths          | `access-log.ts` → `config.excludedPath`        | None (all paths logged)          |
| Custom User-Agent       | `flush-access-logs.mts` headers                | `LimyAnalyticsNetlifyAccessLogs` |

### Payload Format

Your endpoint receives a JSON array of log entries:

```json
[
  {
    "timestamp": "2026-04-01T12:00:00.000Z",
    "method": "GET",
    "url": "https://yoursite.netlify.app/page",
    "path": "/page",
    "query": "",
    "status_code": 200,
    "duration_ms": 45,
    "client_ip": "1.2.3.4",
    "user_agent": "Mozilla/5.0...",
    "referer": null,
    "content_type": "text/html",
    "country": "US",
    "city": "San Francisco",
    "region": "us-west-2",
    "request_id": "...",
    "site": "your-site-name",
    "deploy_id": "..."
  }
]
```

Client-side navigation entries include `"type": "client_navigation"` and do not contain server-side fields like `client_ip`, `country`, or `region`.

### Free Tier Limits

| Resource                     | Limit           |
| ---------------------------- | --------------- |
| Edge Function invocations    | 3M / month      |
| Edge Function CPU time       | 50 ms / request |
| Scheduled Function execution | 30 seconds      |
| Netlify Blobs storage        | 1 GB            |
