SSyncropel Docs
Instance site

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 with theme.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:

ExtensionContent-TypeUsed for
.pngimage/pnglogos, favicons, og images
.jpg, .jpegimage/jpegphotos, og images
.gifimage/gifanimated icons, small graphics
.webpimage/webpmodern image format
.icoimage/x-iconclassic favicons
.woff, .woff2font/woff, font/woff2custom fonts
.csstext/cssadditional 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.html

Worked 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.

On this page