
In my project, I’ve got overlays—things like movement paths, elevation indicators, and token’s boundary—that need to appear only when the viewer has permission to see them. To manage this, I’ve built a system called the OverlayPermissionCoordinator.
It bridges Foundry VTT’s infrastructure with my domain logic, keeping permissions clean, isolated, and easy to reason about.
Here’s a breakdown of how the whole thing works.
Entry Points: Where the Flow Begins
Overlay view permission checks kick off in a few key methods:
graph TD
A[UI/Rendering System] --> B{Which Method?}
B --> C[canViewOverlayOnToken - Single Check]
B --> D[canViewOverlaysOnToken - Batch Check]
B --> E[getViewableOverlaysForToken - Get All Viewable]
Single Overlay Check: canViewOverlayOnToken
This is the core function that checks one overlay on one token. Here's what it does under the hood:
// Check if a single overlay is viewable by the user
canViewOverlayOnToken(
overlayId: string,
targetToken: Token,
userId: string,
isGM: boolean,
viewerTokens: Token[] = []
): boolean {
const config = this.registry.get(overlayId);
if (!config) {
this.logger.warn(`Overlay "${overlayId}" isn’t registered.`);
return false;
}
// Quick exit if the overlay doesn't require permission checks
if (!config.usePermissionSystem) {
return true;
}
// Gather the necessary data for a proper check
const targetData = this.getCachedTokenData(targetToken);
const viewerData = this.getCachedTokensData(viewerTokens);
const allVisibleTokens = this.getAllVisibleTokensData();
// Let the domain service handle the actual decision
return this.permissionService.canViewOverlaysOnToken(
targetData,
userId,
isGM,
viewerData,
allVisibleTokens
);
}
Step-by-step:
Registry Lookup
Pulls the overlay config from the registry
If not found, returns
false
Fast Path Check
If
usePermissionSystem === false, returnstrueimmediatelyThis skips heavy logic for overlays that don’t need it
Data Conversion (Cached)
Converts
targetTokenandviewerTokens[]intoTokenSightDataFetches all visible tokens if needed
Domain Permission Check
Calls
permissionService.canViewOverlays()This service lives in the pure domain layer—no Foundry types involved
Return Result
- Comes back with a
booleanbased on the domain logic
- Comes back with a
Caching Strategy: Keep It Fast, Keep It Simple
To avoid re-converting tokens constantly, I’ve built a lightweight caching layer:
// Token Data Cache Flow
┌─────────────────┐
│ Foundry Token │
└────────┬────────┘
↓
┌────────────────────────┐
│ Generate Cache Key │ → `${token.id}-${cacheGeneration}`
└────────────┬───────────┘
↓
┌────┴────┐
│ Cached? │
└────┬────┘
↓
No ─┴── Yes
↓ ↓
Convert Return
& Store Cached
The key includes a generation number so I can invalidate it whenever needed (e.g. when tokens move or vision updates).
Example Flow: Full Walkthrough
Let’s say a user hovers over a token. I want to know which overlays are visible to them:
getViewableOverlaysForToken(token, userId, isGM, viewerTokens)
Behind the scenes:
Grab all overlays from the registry:
['movement-path', 'elevation-indicator', 'status-effects']Call the batch check:
canViewOverlaysOnToken([...], token, userId, isGM, viewerTokens)For each overlay:
movement-path: needs permission → check domainelevation-indicator: doesn’t need permission → allowstatus-effects: needs permission → check domain
Result:
['elevation-indicator', 'status-effects']
(assumingmovement-pathwas denied)
Why This Works for Me
This setup gives me:
Separation of concerns
Foundry stays out of my domain logic. My domain doesn’t even know Foundry exists.Performance
The system avoids unnecessary work and reuses data wherever possible.Flexibility
I can add new permission types or overlay rules without touching Foundry code.Testability
Domain services are testable in isolation, no mocks needed.Maintainability
All Foundry-specific stuff lives in one place—the coordinator.
If you’re wrangling overlays in a similar way or building on Foundry and trying to decouple things, I think this structure makes it much easier to reason about and test.
Comments
Join the discussionNo comments yet. Be the first to comment.



