Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 40 additions & 8 deletions reference/database/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,15 @@ type MyTable @table {

Optional arguments:

| Argument | Type | Default | Description |
| -------------- | --------- | ----------------------------- | --------------------------------------------------------------------------- |
| `table` | `String` | type name | Override the table name |
| `database` | `String` | `"data"` | Database to place the table in |
| `expiration` | `Int` | — | Seconds until a record goes stale (useful for caching tables) |
| `eviction` | `Int` | `0` | Additional seconds after `expiration` before a record is physically removed |
| `scanInterval` | `Int` | `(expiration + eviction) / 4` | Seconds between eviction scans |
| `replicate` | `Boolean` | true | Enable replication of this table |
| Argument | Type | Default | Description |
| -------------- | --------- | ----------------------------- | ------------------------------------------------------------------------------------------- |
| `table` | `String` | type name | Override the table name |
| `database` | `String` | `"data"` | Database to place the table in |
| `expiration` | `Int` | — | Seconds until a record goes stale (useful for caching tables) |
| `eviction` | `Int` | `0` | Additional seconds after `expiration` before a record is physically removed |
| `scanInterval` | `Int` | `(expiration + eviction) / 4` | Seconds between eviction scans |
| `replicate` | `Boolean` | true | Enable replication of this table |
| `cacheControl` | `String` | — | `Cache-Control` header value emitted on anonymous GET/HEAD 200/304 responses for this table |

**`expiration`, `eviction`, and `scanInterval`**

Expand Down Expand Up @@ -162,6 +163,37 @@ type Event @table(database: "analytics", expiration: 86400) {

**Replication:** Replication is enabled by default for all tables. Note that if you disable replication on a table and re-enable it later, it will not catch-up on previous writes during when the replication was disabled.

#### `cacheControl`

<VersionBadge version="v5.2.0" />

The `cacheControl` argument sets a `Cache-Control` header value emitted on anonymous (unauthenticated) GET/HEAD `200`/`304` responses for this table. It is designed for tables whose content is public and safe for shared caches (CDNs, reverse proxies) to store.

```graphql
type Product @table(cacheControl: "public, max-age=60") @export {
id: Long @primaryKey
name: String
price: Float
}
```

Key semantics:

- **Anonymous reads only.** The header is emitted only when the request carries no authenticated principal. Authenticated responses instead receive an identity floor of `Cache-Control: private, no-cache` (plus `Vary: Authorization`/`Cookie`) — the table declaration is not inherited by authenticated reads.
- **Explicit opt-in required.** Anonymous readability alone does not cause Harper to emit shared-cache headers, because a table gated on request attributes (IP, headers, etc.) could leak across a URL-keyed cache. The `cacheControl` declaration is the explicit statement that the content is safe to cache publicly.
- **Never on 401 responses.** A rejected request always gets `Cache-Control: private, no-cache` regardless of any declaration.
- **Visible in `describe_table`.** The value is stored with the table schema and surfaces in the Operations API's `describe_table` output.

The declaration can equivalently be made as a `static cacheControl` property on an exported JavaScript resource class:

```javascript
export class Product extends tables.Product {
static cacheControl = 'public, max-age=60';
}
```

See [REST Headers / Cache-Control](../rest/headers.md#cache-control) for the full caching behavior and how `Cache-Control` interacts with authentication headers.

### `@export`

Exposes the table as an externally accessible resource endpoint, available via REST, MQTT, and other interfaces.
Expand Down
2 changes: 2 additions & 0 deletions reference/http/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ Default: `"Accept, Content-Type, Authorization"`

Comma-separated list of headers allowed in the [`Access-Control-Allow-Headers`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers) response header for OPTIONS (preflight) requests.

<VersionBadge version="v5.2.0" /> When CORS is enabled, Harper emits `Vary: Origin` on all responses (including no-origin requests whose `Access-Control-Allow-Origin`-less variant is also origin-dependent), so that shared caches key responses correctly by origin.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For consistency with other documentation pages in this repository, the <VersionBadge> component should be placed on its own line with surrounding blank lines rather than inline with the text.

Suggested change
<VersionBadge version="v5.2.0" /> When CORS is enabled, Harper emits `Vary: Origin` on all responses (including no-origin requests whose `Access-Control-Allow-Origin`-less variant is also origin-dependent), so that shared caches key responses correctly by origin.
<VersionBadge version="v5.2.0" />
When CORS is enabled, Harper emits `Vary: Origin` on all responses (including no-origin requests whose `Access-Control-Allow-Origin`-less variant is also origin-dependent), so that shared caches key responses correctly by origin.


## Session Affinity

### `http.sessionAffinity`
Expand Down
33 changes: 33 additions & 0 deletions reference/rest/headers.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,39 @@ These headers are included in all Harper REST API responses:
| `etag` | `"abc123"` | Encoded version/last-modification time of the returned record. Used for conditional requests. |
| `location` | `/MyTable/new-id` | Returned on `POST` responses. Contains the path to the newly created record. |

## Cache-Control

<VersionBadge version="v5.2.0" />

Harper applies a tiered Cache-Control policy to REST responses.

### Anonymous reads

For tables (or JS resource classes) that declare a `cacheControl` value, Harper emits that value verbatim on anonymous (unauthenticated) GET/HEAD `200`/`304` responses:

```
Cache-Control: public, max-age=60
```

See [`@table(cacheControl: "...")`](../database/schema.md#cachecontrol) for how to declare this value in your schema.

### Authenticated reads (identity floor)

Any response where a principal was resolved — or where credentials were rejected (including `401` and the login-redirect `302`) — receives an identity floor regardless of any table declaration:

```
Cache-Control: private, no-cache
Vary: Authorization
```

When cookie-based sessions are in use, `Cookie` is appended to `Vary`.

An app may explicitly set a `public` or `s-maxage` directive in a resource handler to opt an authenticated response into shared caching (per RFC 9111). The opt-in is trusted, except on `401` responses: a rejected request always receives `private, no-cache`.

### CORS responses

When CORS is enabled, responses that reflect an `Origin` header also emit `Vary: Origin` so that shared caches key correctly by origin. See [CORS configuration](../http/configuration.md#cors).

## Request Headers

### Content-Type
Expand Down
34 changes: 34 additions & 0 deletions reference/static-files/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,40 @@ In addition to the standard `files`, `urlPath`, and `timeout` options, `static`

- **`notFound`** - `string | { file: string; statusCode: number }` - _optional_ - A custom file (or file + status code) to return when a path is not found. Useful for serving a custom 404 page or for SPAs that use client-side routing.

### Cache Headers

<VersionBadge version="v5.2.0" />

Static files are served before authentication runs, so they are public by construction. Use the following options to control the `Cache-Control` header Harper emits for served files:

- **`maxAge`** - `number` (seconds) or `string` (duration) - _optional_ - Freshness lifetime for served files. A number is treated as seconds; a string like `'5m'` or `'1d'` is parsed as a duration. Emits `Cache-Control: public, max-age=<seconds>`. Defaults to `0` (revalidate on every request via ETag/Last-Modified). Malformed values throw at startup rather than silently degrading. Duration suffixes are `y` (year), `M` (30-day month), `d` (day), `h` (hour), and `m` (minute) — note the case-sensitive distinction between `M` (month) and `m` (minute).

- **`immutable`** - `boolean` - _optional_ - When `true`, appends the `immutable` directive to the `Cache-Control` header. Use this for content-hashed assets whose URL changes when the content changes, so browsers and CDNs never revalidate them. Defaults to `false`.

- **`cacheControl`** - `string | false` - _optional_ - Full `Cache-Control` override string. Takes precedence over `maxAge` and `immutable` when set. Set to `false` to suppress the `Cache-Control` header entirely.

- **`cacheOverrides`** - `object` - _optional_ - A map of glob pattern → per-file cache options (any of `maxAge`, `immutable`, `cacheControl`), letting specific files opt out of the top-level defaults. The typical case is long-lived `immutable` defaults for content-hashed assets while `index.html` gets a short window or `stale-while-revalidate`. Patterns use the same [`micromatch`](https://github.com/micromatch/micromatch) engine as `files`, and are matched against both the request URL path (relative to the mount) and the served file's basename — so `index.html` also targets the directory-index (`/`) response. Entries are tested in config order and the **first match wins**; each entry is a partial — options it sets replace the top-level default, options it omits are inherited, with the same `cacheControl`-over-`maxAge`/`immutable` precedence.

The `notFound` fallback always uses `max-age=0` regardless of `maxAge`, which is correct for SPA index fallbacks — a `200 index.html` response should revalidate so updated builds are picked up promptly.

Example: serve a build directory with long-lived immutable caching for hashed assets, while the entry `index.html` revalidates every request (optionally serving stale content while it does):

```yaml
static:
files: 'web/**'
maxAge: '1y'
immutable: true
cacheOverrides:
# First match wins; index.html is matched by basename on the `/` serve.
'index.html':
cacheControl: 'public, max-age=0, stale-while-revalidate=60'
'*.html':
maxAge: '5m'
immutable: false
```

> `stale-while-revalidate` support varies by CDN — CloudFront and Cloudflare honor it, Azure CDN does not. Where it is unsupported the directive is simply ignored (the resource is treated as `max-age=0`), so it is safe to set.

## Auto-Updates

<VersionBadge version="v4.7.0" />
Expand Down