PHPackages                             brynforum/api-cache - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Caching](/categories/caching)
4. /
5. brynforum/api-cache

ActiveFlarum-extension[Caching](/categories/caching)

brynforum/api-cache
===================

Server-side response caching for Flarum's API. Configure regex-pattern rules with per-rule TTLs to short-circuit expensive endpoints (top-poster widgets, statistics aggregations, etc.) before they touch the database.

v0.1.0(3w ago)05MITPHPCI passing

Since May 16Pushed 3w agoCompare

[ Source](https://github.com/BrynForum/flarum-ext-api-cache)[ Packagist](https://packagist.org/packages/brynforum/api-cache)[ RSS](/packages/brynforum-api-cache/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (2)Versions (2)Used By (0)

brynforum/api-cache
===================

[](#brynforumapi-cache)

[![Latest Version on Packagist](https://camo.githubusercontent.com/44722a6aae9fc3bd6a848357e5b935d0c43e7d76974069e5920bec6f38357ba0/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6272796e666f72756d2f6170692d63616368652e7376673f63616368655365636f6e64733d33363030)](https://packagist.org/packages/brynforum/api-cache)[![Total Downloads](https://camo.githubusercontent.com/07209882fff3782e929e23a870cdcfcb8c8654d7d9784bd90ec9926896d464ae/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6272796e666f72756d2f6170692d63616368652e7376673f63616368655365636f6e64733d33363030)](https://packagist.org/packages/brynforum/api-cache)[![License](https://camo.githubusercontent.com/1804159efc6f12b45d3bad284054e3addf5749313e25e27e7df877e5bb3ad95d/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f6272796e666f72756d2f6170692d63616368652e7376673f63616368655365636f6e64733d33363030)](LICENSE)

A [Flarum](https://flarum.org) extension that adds **server-side response caching** to the JSON:API. Configure regex-pattern rules with per-rule TTLs to short-circuit expensive endpoints — top-poster widgets, statistics aggregations, public discussion lists — before they touch the database.

Built and used in production by [BrynForum](https://brynforum.com).

Why
---

[](#why)

Flarum re-aggregates SQL on every request to certain API endpoints. The canonical example is `/api/users?filter[top_poster]=true` — the popular [afrux/top-posters-widget](https://github.com/afrux/top-posters-widget) hits it on every page load, and the answer changes once an hour, not once a request.

Edge-caching the response at Cloudflare is a workaround at the wrong layer (and on free/pro plans, Set-Cookie blocks caching anyway). The right fix is caching inside Flarum, where the rules can be tuned per-tenant in admin and the cache key can include exactly what matters.

Installation
------------

[](#installation)

```
composer require brynforum/api-cache
```

Then enable **API Cache** under Admin → Extensions.

The extension creates a `brynforum_api_cache_rules` table on first enable.

Optional: Redis backend
-----------------------

[](#optional-redis-backend)

The extension uses Flarum's default file cache out of the box. To use Redis (recommended for busy forums), set these environment variables on the container:

```
REDIS_HOST=your-redis-host
REDIS_PORT=6379          # default
REDIS_PASSWORD=secret    # optional
REDIS_DB=0               # default

```

The extension auto-detects the env vars, pings the host, and uses Redis if reachable — otherwise transparently falls back to the file driver. Check the active backend via the `X-BrynForum-Cache-Backend: redis|file` response header.

PHP-FPM defaults to `clear_env = yes` on most distros. Set `clear_env = no` in your `php-fpm.conf` so PHP can read the env vars at runtime.

How it works
------------

[](#how-it-works)

A PSR-15 middleware sits on Flarum's API stack. For each `GET` request:

1. Walks active rules in priority order; checks the path against `path_pattern` (PCRE regex) and the querystring against `query_filter` (optional regex). First match wins.
2. Computes a stable cache key: `sha1(method + path + sorted(query) + scope)`.
3. On **HIT**: returns the cached `200` response (with original headers minus `Set-Cookie`). No handler invocation, no DB queries.
4. On **MISS**: runs the handler. If the response is `200`, caches it with the rule's TTL.

Configuration
-------------

[](#configuration)

Admin → API Cache. Add rules with:

FieldTypeDescription**Name**textYour label.**Path pattern**PCRE regexRequired. Includes delimiters, e.g. `#^/api/users$#`.**Query filter**PCRE regexOptional. Matched against the raw querystring, e.g. `#filter\[top_poster\]=true#`. Empty = match any querystring.**TTL (seconds)**intHow long to keep the cached response (1 – 604800).**Scope**`public` | `guest`See below.**Priority**intHigher = checked first. Tiebreaker when multiple rules match.**Enabled**boolToggle without deleting.**Allow authenticated requests to seed cache**bool, default onWhen off, only responses generated for logged-out visitors are written into the cache. Everyone can still read the cached payload. Use on endpoints whose response varies subtly between guests and admins — e.g. `/api/users` includes `email` when the requester is an admin. See "When NOT to cache".There's a **Clear all cache** button to invalidate everything in one click.

### Scopes

[](#scopes)

- **public** — cache shared across all visitors. Use for endpoints whose response doesn't depend on identity (top-poster lists, public stats, discussion index when no per-user filter applies).
- **guest** — cache used only when the request has no session. Authenticated users bypass the cache entirely, so their identity-specific responses never land in a shared bucket.

Verifying it works
------------------

[](#verifying-it-works)

Every matched response goes out with two debug headers:

- `X-BrynForum-Cache` — one of:
    - `HIT` — served from cache, handler never ran.
    - `MISS` — handler ran, response stored.
    - `SKIP-status` — handler ran, response not stored (non-200).
    - `SKIP-private` — handler ran, response not stored (handler returned `Cache-Control: private` or `no-store`).
    - `SKIP-authseed` — handler ran for an authenticated request, response not stored because the rule disallows authenticated seeding (other auth users get fresh handler responses; only guest requests warm this cache).
- `X-BrynForum-Cache-Backend: redis | file` — which backend is active.

Example:

```
$ curl -sI 'https://forum.example.com/api/discussions' | grep -i x-bryn
x-brynforum-cache: MISS
x-brynforum-cache-backend: redis

$ curl -sI 'https://forum.example.com/api/discussions' | grep -i x-bryn
x-brynforum-cache: HIT
x-brynforum-cache-backend: redis
```

Safety properties
-----------------

[](#safety-properties)

The extension enforces these regardless of how rules are configured:

- Only `200 OK` responses are cached. Errors, 4xx and 5xx pass through untouched.
- Only `GET` requests are eligible. Non-GET passes through.
- `Set-Cookie` is stripped from cached responses (avoids session leakage).
- The cache key does **not** include `Authorization` headers or session cookies — only `path + querystring + scope`. This is intentional: `public` scope means "same for everyone"; `guest` scope means "only used when unauthenticated".
- Rule list itself is cached for 60 s and explicitly invalidated when rules change, so admin edits take effect quickly.
- **`Cache-Control: private` and `Cache-Control: no-store` are honoured.** If the route handler explicitly marks a response per-user (Flarum or any extension), it's never cached even if the rule says `scope=public`. The response goes out with `X-BrynForum-Cache: SKIP-private`.
- **The rule validator refuses `scope=public` for known per-user paths** — patterns matching `/api/users/`, `/api/users/me`, `/api/notifications`, `/api/preferences`, `/api/access_tokens`. Operator gets a clear error pointing them at `scope=guest`.
- **Per-rule "guest-only seed" mode.** Each rule has an *Allow authenticated requests to seed cache* flag. When off, authenticated requests bypass the *write* path (their response is returned but not stored); everyone still reads from cache when it's warm. Closes the "admin's response, with `email` included, lands in a shared bucket guests subsequently read" hole on endpoints like `/api/users?filter[top_poster]=true`. Surfaces as `X-BrynForum-Cache: SKIP-authseed` when an auth request would have seeded but didn't.

When NOT to cache
-----------------

[](#when-not-to-cache)

Some endpoints respond differently to different users even at the same URL. Caching them at `scope=public` will leak one user's data to another. **Use `scope=guest` (or don't cache at all)** for:

Endpoint patternWhy it's per-user`/api/users/`Profile incl. email/preferences may be visible to the owner only.`/api/users/me`Always the current user.`/api/notifications`, `/api/notifications/`Per-user notification feed.`/api/preferences`Per-user prefs.`/api/access_tokens`The user's API tokens.`/api/discussions` *(sometimes)*The list response is mostly shared, but if a logged-in user is making the request, fields like `lastReadPostId` and `subscription` are mixed in. **Safe at `scope=guest`, not at `scope=public`.**`/api/posts//edits`Edit history may include drafts.**Safe at `scope=public`:**

Endpoint patternWhy it's safe`/api/users?filter[top_poster]=true`List, no per-user state.`/api/tags`Public taxonomy.`/api/statistics`Site-wide aggregates.Custom public endpoints (`/api/brynforum/top-posters` etc.)If the extension's response doesn't vary by viewer.If you're unsure, set the rule to `scope=guest` first and verify the response shape is identical to what a logged-out visitor sees. Promote to `scope=public` only once you're certain there's no per-user variation.

Example rules
-------------

[](#example-rules)

Use casePath patternQuery filterTTLScopeTop-poster widget`#^/api/users$#``#filter\[top_poster\]=true#`600publicForum index (logged-out)`#^/api/discussions$#`—60guestTag list`#^/api/tags$#`—3600publicSite statistics endpoint`#^/api/statistics$#`—300publicContributing
------------

[](#contributing)

Issues and PRs welcome. The extension is intentionally small and focused; before opening a large PR, please file an issue to discuss.

License
-------

[](#license)

[MIT](LICENSE) © BrynForum

###  Health Score

33

—

LowBetter than 73% of packages

Maintenance95

Actively maintained with recent releases

Popularity5

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity23

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

24d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/7a2a5ea12a797d7e1b0f38c0eb603b6f50c22564951bf9172baa0afcbdc05615?d=identicon)[wimdows-nl](/maintainers/wimdows-nl)

---

Top Contributors

[![wimdows-nl](https://avatars.githubusercontent.com/u/24444562?v=4)](https://github.com/wimdows-nl "wimdows-nl (3 commits)")

### Embed Badge

![Health badge](/badges/brynforum-api-cache/health.svg)

```
[![Health](https://phpackages.com/badges/brynforum-api-cache/health.svg)](https://phpackages.com/packages/brynforum-api-cache)
```

###  Alternatives

[rhubarbgroup/redis-cache

A persistent object cache backend for WordPress powered by Redis. Supports Predis, PhpRedis, Relay, replication, sentinels, clustering and WP-CLI.

52799.8k1](/packages/rhubarbgroup-redis-cache)[flarum-lang/russian

Russian language pack for Flarum.

12127.5k](/packages/flarum-lang-russian)[symfony-bundles/redis-bundle

Symfony Redis Bundle

291.2M6](/packages/symfony-bundles-redis-bundle)[pdffiller/qless-php

PHP Bindings for qless

29113.7k1](/packages/pdffiller-qless-php)[millipress/millicache

WordPress Full-Page Cache based on Rules &amp; Flags. Delivers flexible, scalable caching workflows backed by Redis and ValKey in-memory stores.

622.0k2](/packages/millipress-millicache)[flarum-lang/french

French language pack to localize the Flarum forum software plus its official and third-party extensions.

1936.5k](/packages/flarum-lang-french)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
