Filesystem overlay (Tier 3)
Drop CSS, assets, and a small <head> allowlist at ~/.syncro/site/ and the renderer picks them up on each request. The WordPress-theme-style escape hatch for full visual control.
Tier 3 is the filesystem-overlay layer of the instance site.
The renderer reads three optional locations under ~/.syncro/site/ on
each request, with a 5-second LRU cache at the process level. You can
override the accent palette, add a custom <head> fragment, and serve
operator-supplied assets — all without modifying the engine or
republishing records.
Use Tier 3 when you need:
- Custom typography or fonts beyond the L0 default stack
- A different accent color across both light and dark themes
- Open Graph images, a favicon, or operator-supplied logos
- Custom
<meta>tags (Twitter cards, link tags) within the strict allowlist
Tier 3 layers on top of Tier 2 (content overrides). The L0 floor is always preserved.
The directory layout
~/.syncro/site/
├── theme.css # extra CSS, inlined after the L0 stylesheet
├── head.html # extra <head> elements (strict allowlist)
└── assets/ # operator-supplied assets, served at /site/local/<name>
├── logo.png
├── og-image.jpg
├── favicon.ico
└── ...Every entry is optional. Drop only what you need.
theme.css — override visual variables
The L0 identity card's typography, colors, and structural spacing all
flow from CSS custom properties on :root. The renderer inlines them
ahead of any theme.css content, so anything you put in theme.css
wins via the cascade.
The variables you can safely override:
:root {
/* Accent — used for the kind label, the Connect button, link hover */
--accent: #c2410c;
--accent-strong: #9a3412;
--accent-fg: #ffffff;
/* Glyph palette — the generative glyph in the masthead */
--g1: #c2410c;
--g2: #1f9d57;
--g3: #b9842a;
--g4: #7a5ca6;
--g5: #b23c54;
}
@media (prefers-color-scheme: dark) {
:root {
--accent: #fb923c;
--accent-strong: #fdba74;
/* ... */
}
}You can also load a custom font and re-bind the typography variables:
@font-face {
font-family: 'Acme Sans';
src: url('/site/local/AcmeSans-Regular.woff2') format('woff2');
font-display: swap;
}
:root {
--sans: 'Acme Sans', system-ui, sans-serif;
}Notice the asset URL: /site/local/AcmeSans-Regular.woff2. That's the
operator-asset route — see below.
Budget
theme.css content is inlined into the response HTML, so it counts
against the per-render HTML weight budget (under 100 KB total). Keep
the overlay small — a few hundred lines of focused overrides is
plenty. If you need a lot of CSS, that's a sign you're building a UI;
build it in a workspace instead.
head.html — extra <head> content
The renderer parses head.html against a strict allowlist before
inlining it into the response. Only three element kinds are accepted:
<meta>— for Open Graph, Twitter cards, theme-color, etc.<link>— for favicons, preconnect, manifest, RSS, atom feeds<style>— additional inline CSS (combined withtheme.css)
Anything else — <script>, <noscript>, <iframe>, raw text nodes —
is rejected at parse time. The reject is silent at the request layer
(the head fragment is dropped); the instance's structured log records the
rejection with the offending tag for diagnosis. There is no path to
inject JavaScript via Tier 3.
Example head.html:
<meta property="og:image" content="https://acme.syncropel.app/site/local/og-image.jpg">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta name="twitter:card" content="summary_large_image">
<link rel="icon" type="image/png" href="/site/local/favicon.png">
<link rel="alternate" type="application/atom+xml" title="Atom feed" href="/feed.atom">These flow into the response between the engine-emitted <meta> tags
and the closing </head>.
assets/ — operator-supplied files at /site/local/<name>
Anything under ~/.syncro/site/assets/ is served by the engine at
/site/local/<name> — same origin as the landing page, no auth
required, content-type negotiated from the file extension.
Allowed file extensions and their content types:
| Extension | Content-Type | Used for |
|---|---|---|
.png | image/png | logos, favicons, og images |
.jpg, .jpeg | image/jpeg | photos, og images |
.gif | image/gif | animated icons, small graphics |
.webp | image/webp | modern image format |
.ico | image/x-icon | classic favicons |
.woff, .woff2 | font/woff, font/woff2 | custom fonts |
.css | text/css | additional stylesheets (rare; prefer theme.css) |
Notably not allowed: .svg, .js, .html, .htm, .xml,
.json. SVG is excluded because SVG can carry executable scripts;
JavaScript and HTML are excluded by design to keep the rendered surface
script-free. If you need a vector logo, rasterize it as .png or
.webp.
The Content-Type header is set strictly from the extension. The
X-Content-Type-Options: nosniff header is set on every response. A
strict CSP (default-src 'none') is set on each asset response to
prevent the asset itself from initiating any sub-requests.
Cache + invalidation
Asset responses carry Cache-Control: public, max-age=3600. To
invalidate browser caches after replacing an asset, rename it (logo.png
→ logo-v2.png) or append a query string in your theme.css / head.html
references (/site/local/logo.png?v=2). The engine does not version
operator-supplied assets — that's the operator's responsibility.
Reload semantics
The Tier 3 overlay is read on each render request and cached for 5
seconds at the process level. Drop a file, wait up to 5 seconds, hit
GET / — the new overlay is live. No instance restart, no record emit.
To verify what the renderer is reading right now:
spl site preview --out /tmp/preview.html
# Open /tmp/preview.html in a browser, or grep for what you expect:
grep -E '<meta|<link|--accent' /tmp/preview.htmlWorked example — accent + favicon + custom font
Goal: switch the instance accent to a brand orange, swap the favicon, and load the brand's custom sans font.
mkdir -p ~/.syncro/site/assets
# Drop the font + favicon
cp brand-sans-regular.woff2 ~/.syncro/site/assets/
cp brand-favicon.png ~/.syncro/site/assets/
# Write theme.css
cat > ~/.syncro/site/theme.css <<'CSS'
@font-face {
font-family: 'Brand Sans';
src: url('/site/local/brand-sans-regular.woff2') format('woff2');
font-display: swap;
}
:root {
--accent: #c2410c;
--accent-strong: #9a3412;
--sans: 'Brand Sans', system-ui, sans-serif;
}
@media (prefers-color-scheme: dark) {
:root {
--accent: #fb923c;
--accent-strong: #fdba74;
}
}
CSS
# Wire up the favicon
cat > ~/.syncro/site/head.html <<'HTML'
<link rel="icon" type="image/png" href="/site/local/brand-favicon.png">
HTML
# Verify
curl -s https://your-instance.syncropel.app/ | grep -E 'icon|--accent|Brand Sans'Within 5 seconds of dropping the files, the next GET / will pick them
up.
When NOT to use Tier 3
- If you only need to set the instance name and mission, use Tier 2 content overrides — it's portable, federates, and shows up in the record log.
- If your visual customization gets into "I want my own homepage" — at that point you want either a workspace (records + projection) or Studio (a real interactive app). The instance site is an identity card; treat it like the lobby, not the building.
- If you find yourself wanting to write JavaScript to make the page interactive: don't. The instance site is intentionally script-free. Build the interactive surface in a workspace or in Studio.
Content overrides (Tier 2)
Author a core.site.v1 record to override the L0 identity card's content slots — name, mission, links, Connect button. The spl site family wraps the lifecycle; the record shape is portable.
Installing & managing workspaces
Install a workspace from a document file, list what your instance serves, inspect one, update it, and archive it — all at runtime, with no rebuild.