> 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            |


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.getlimy.ai/limy-pixel/cdn-integration/netlify-free-plan.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
