This is an automated email from the ASF dual-hosted git repository.
hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin-website.git
The following commit(s) were added to refs/heads/master by this push:
new e09f6509b docs: add production deployment guide
e09f6509b is described below
commit e09f6509b38d3ed64e06e963ba22941591c42c93
Author: Yang Luo <[email protected]>
AuthorDate: Wed Oct 7 11:21:02 2026 +0800
docs: add production deployment guide
---
docs/MultiThreading.mdx | 2 +
docs/ProductionDeployment.mdx | 125 ++++++++++++++++++++++++++++++++++++++++++
sidebars.js | 1 +
3 files changed, 128 insertions(+)
diff --git a/docs/MultiThreading.mdx b/docs/MultiThreading.mdx
index 660e69e16..a7b69a379 100644
--- a/docs/MultiThreading.mdx
+++ b/docs/MultiThreading.mdx
@@ -9,3 +9,5 @@ authors: [hsluoyz]
In **multi-threaded** code, use Casbin’s **synchronized enforcer** so one
enforcer can be shared safely across threads:
[SyncedEnforcer](https://github.com/apache/casbin/blob/master/enforcer_synced.go)
(Go),
[enforcer_synced.cpp](https://github.com/apache/casbin-cpp/blob/master/casbin/enforcer_synced.cpp)
(C++).
The synced enforcer can also **auto-reload** policy: call
`StartAutoLoadPolicy()` to poll the policy source and reload when it changes;
call `StopAutoLoadPolicy()` to stop.
+
+For running several instances, caching and keeping policy in sync, see
[Production Deployment](/docs/production-deployment).
diff --git a/docs/ProductionDeployment.mdx b/docs/ProductionDeployment.mdx
new file mode 100644
index 000000000..64b046754
--- /dev/null
+++ b/docs/ProductionDeployment.mdx
@@ -0,0 +1,125 @@
+---
+id: production-deployment
+title: Production Deployment
+description: "Run Casbin in production - one enforcer per process, thread
safety with SyncedEnforcer, decision caching, keeping replicas in sync with
watchers, and writing policy inside database transactions."
+keywords: [production, deployment, SyncedEnforcer, watcher, replicas, thread
safety, LoadPolicy]
+authors: [hsluoyz]
+---
+
+Casbin is a library: the enforcer lives inside your process and keeps the
whole policy in memory. That makes `Enforce()` fast, but it also means **you**
decide how the enforcer is created, shared between threads, and kept in sync
with other instances. This page is a checklist for doing that correctly.
+
+## Create one enforcer per process
+
+Create the enforcer once at startup and share it. Loading policy is expensive:
it reads every rule from the adapter and rebuilds the model and the role graph.
+
+Do **not** call `LoadPolicy()` (or create a new enforcer) on every request:
+
+```go
+// Don't: this reads the whole policy table on every request
+func authorize(sub, obj, act string) (bool, error) {
+ if err := e.LoadPolicy(); err != nil {
+ return false, err
+ }
+ return e.Enforce(sub, obj, act)
+}
+```
+
+Policy changes made through the enforcer's [Management
API](/docs/management-api) (`AddPolicy`, `RemovePolicy`, `AddGroupingPolicy`,
...) update memory and, with [AutoSave](/docs/adapters#autosave), the database
at the same time. Changes made by **other** instances reach this one through a
[watcher](#keep-replicas-in-sync). There is no need to reload before enforcing.
+
+If the policy is too large to keep in memory, load only the part each instance
needs with [Policy subset loading](/docs/policy-subset-loading).
+
+## Share the enforcer safely between threads
+
+The plain `Enforcer` is **not** safe for concurrent use when policy can change
while requests are being enforced: a `LoadPolicy()` or `AddPolicy()` running on
one thread can race with `Enforce()` on another. Use the synchronized enforcer,
which guards reads and writes with a read-write lock:
+
+| Language | Synchronized enforcer |
+|----------|-----------------------|
+| Go | `casbin.NewSyncedEnforcer()`, `casbin.NewSyncedCachedEnforcer()` |
+| Java | `SyncedEnforcer`, `SyncedCachedEnforcer` |
+| Python | `casbin.SyncedEnforcer` |
+| Node.js | `newSyncedEnforcer()` |
+| C++ | `casbin::SyncedEnforcer` |
+
+The synchronized enforcer has the same API as `Enforcer`. See
[Multithreading](/docs/multi-threading) and [Enforcers](/docs/enforcers) for
details.
+
+## Cache decisions carefully
+
+`CachedEnforcer` and `SyncedCachedEnforcer` remember the result of each
`Enforce()` call. They help when the same requests repeat often, but a cached
decision can outlive the policy that produced it. In Go:
+
+- `LoadPolicy()` clears the whole cache.
+- Adding or removing a policy rule drops at most the cache entry whose request
matches that rule exactly. Decisions that depended on the rule indirectly, for
example through a role assignment (`g`), stay cached.
+- Call `InvalidateCache()` after changing role assignments, and set a time
limit with `SetExpireTime()` so no stale decision lives forever.
+
+## Keep replicas in sync
+
+When you run more than one instance (several pods, or several worker processes
per pod), each one has its own copy of the policy. Without synchronization, a
permission granted through instance A is invisible to instance B, and, worse, a
permission **revoked** through A keeps working on B until B restarts.
+
+Use a [watcher](/docs/watchers) (Redis, etcd, Kafka, NATS, a database, ...).
When an instance changes policy, it publishes a message; the other instances
receive it and update their copy.
+
+```go
+import (
+ "github.com/casbin/casbin/v3"
+ gormadapter "github.com/casbin/gorm-adapter/v3"
+ rediswatcher "github.com/casbin/redis-watcher/v2"
+)
+
+a, _ := gormadapter.NewAdapterByDB(db)
+e, _ := casbin.NewSyncedCachedEnforcer("rbac_model.conf", a)
+
+w, _ := rediswatcher.NewWatcher("redis:6379", rediswatcher.WatcherOptions{
+ Channel: "/casbin",
+ IgnoreSelf: true,
+})
+_ = e.SetWatcher(w)
+
+// Reload through the synced, cached enforcer so the reload takes its lock
+// and clears its decision cache
+_ = w.SetUpdateCallback(func(string) { _ = e.LoadPolicy() })
+```
+
+Things to get right:
+
+- **Set the callback yourself when you use a wrapped enforcer.** In Go,
`SetWatcher()` installs a default callback that reloads the inner `Enforcer`,
which bypasses the lock of `SyncedEnforcer` and the cache of
`SyncedCachedEnforcer`. Call `SetUpdateCallback()` after `SetWatcher()`, as
above.
+- **AutoSave and notification are independent.** `EnableAutoSave()` decides
whether a change is written to the adapter; `EnableAutoNotifyWatcher()` decides
whether other instances are told about it. You can turn off AutoSave and still
notify, which is what you need when you persist policy yourself (see
[below](#write-policy-inside-your-own-transaction)). pycasbin 2.8.0 and earlier
only notify when AutoSave is on; newer versions behave like the other languages.
+- **Incremental updates.** A plain `Watcher` makes the other instances reload
the whole policy. A `WatcherEx` sends the exact change (`UpdateForAddPolicy`,
`UpdateForRemovePolicy`, ...), and the receiving instance applies only that
change. In Python, apply it with `DistributedEnforcer.add_policy_self(False,
...)` and friends, which update memory without writing to the adapter. In Go,
the `Self*` methods (`SelfAddPolicy`, `SelfRemovePolicy`, ...) skip the watcher
but still write to the ad [...]
+- **Have a backstop.** Messages can be lost while an instance is disconnected
from the message bus. A periodic full reload bounds how long a missed update
can last: `StartAutoLoadPolicy(5 * time.Minute)` on a `SyncedEnforcer` (Go,
Java and Python have it).
+- **Need strong consistency?** A watcher is eventually consistent. If every
instance must see a change before the write returns, use a
[dispatcher](/docs/dispatchers), or move authorization into one service with
[Casbin Server](/docs/service).
+
+## Write policy inside your own transaction
+
+Applications often create a user, a team and the user's role in one database
transaction. If the enforcer writes `casbin_rule` with AutoSave on, that write
is outside your transaction, and if the transaction rolls back, the rule stays.
+
+Two ways to keep them together:
+
+1. **Write the rows yourself, then update memory after commit.** Turn AutoSave
off, insert the `casbin_rule` rows in your transaction, and after it commits
call the enforcer's `AddPolicy()` / `AddGroupingPolicy()`. With AutoSave off
these only update memory, and the watcher still notifies the other instances.
+
+ ```python
+ enforcer.enable_auto_save(False)
+
+ with db.transaction():
+ create_user(...)
+ insert_casbin_rule("g", "alice", "team-admin")
+
+ # after commit: update this instance and notify the others
+ enforcer.add_grouping_policy("alice", "team-admin")
+ ```
+
+2. **Go: use the transactional enforcer.** `NewTransactionalEnforcer()` with a
transactional adapter (for example `gormadapter.NewTransactionalAdapterByDB()`)
buffers changes and applies them to the database and to memory on `Commit()`.
See [Adapters: Transaction](/docs/adapters#transaction). A commit does not
notify the watcher, so call `w.Update()` after it to tell the other instances.
+
+## Read-only replicas and migrations
+
+Some adapters create the `casbin_rule` table when they start. On a read-only
database replica (for example a passive disaster-recovery site) that statement
fails. Create the table with your own migrations and turn the adapter's
migration off where the adapter supports it, for example
`gormadapter.TurnOffAutoMigrate(db)` in Go. On read-only instances, only call
`LoadPolicy()` and `Enforce()`.
+
+## Fail closed
+
+Treat any error as a denial:
+
+- If the enforcer cannot be created or the policy cannot be loaded at startup,
refuse to serve requests that need authorization instead of letting them
through.
+- If `Enforce()` returns an error, deny the request.
+
+```go
+ok, err := e.Enforce(sub, obj, act)
+if err != nil || !ok {
+ return http.StatusForbidden
+}
+```
diff --git a/sidebars.js b/sidebars.js
index c2af775f6..dbef32920 100644
--- a/sidebars.js
+++ b/sidebars.js
@@ -146,6 +146,7 @@ module.exports = {
label: "Advanced Usage",
link: {type: "generated-index"},
items: [
+ "production-deployment",
"multi-threading",
"benchmark",
"performance",
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]