364 lines
20 KiB
Cheetah
364 lines
20 KiB
Cheetah
{{define "hosted"}}
|
|
<!-- Hero -->
|
|
<div class="hero container">
|
|
<p class="label accent mb-4"><span class="vaultname">clavitor</span> hosted</p>
|
|
<h1>Zero cache. Every request hits the vault.</h1>
|
|
<p class="lead">Clavitor never caches credentials — not in memory, not on disk, not anywhere. Every request is a fresh decrypt from the vault. That's the security model. To make it fast, we run {{len .Pops}} regions across every continent. Your data lives where you choose. <s>$20</s> $12/yr.</p>
|
|
</div>
|
|
|
|
<!-- Map -->
|
|
<div class="container">
|
|
<div class="map-wrap">
|
|
<svg id="worldmap" viewBox="0 0 1000 460" xmlns="http://www.w3.org/2000/svg">
|
|
<image href="/worldmap.svg" x="0" y="0" width="1000" height="460"/>
|
|
<text x="500" y="440" font-family="Figtree,sans-serif" font-size="18" font-weight="700" fill="#0A0A0A" text-anchor="middle" opacity="0.35" letter-spacing="0.3em">CLAVITOR GLOBAL PRESENCE</text>
|
|
</svg>
|
|
</div>
|
|
<div class="mt-4" style="display:flex;justify-content:space-between;align-items:baseline;gap:16px;flex-wrap:wrap">
|
|
<p class="text-sm text-tertiary" style="margin:0">We have strategically chosen our datacenter locations so that almost every place on the planet gets an answer within 60 ms. If your location is slower than that, <a href="mailto:support@clavitor.com">reach out</a> and we will work on a solution.</p>
|
|
<button id="backup-toggle" class="btn btn-ghost" style="white-space:nowrap;font-size:0.75rem;padding:6px 12px">Show backup routes</button>
|
|
</div>
|
|
<div class="mt-12"></div>
|
|
<div id="dc-grid" class="mb-8">
|
|
<!-- Self-hosted -->
|
|
<div class="dc-card red" data-lon="-999">
|
|
<div class="dc-icon">🖥️</div>
|
|
<div class="dc-name">Self-hosted</div>
|
|
<div class="dc-sub">Your machine. Your rules.</div>
|
|
<div class="dc-status"><span class="dc-dot"></span>Free forever</div>
|
|
<a href="/install" class="btn btn-red btn-block">Download now →</a>
|
|
</div>
|
|
<!-- Zürich HQ -->
|
|
<div class="dc-card gold" data-lon="8.5">
|
|
<div class="dc-icon">🇨🇭</div>
|
|
<div class="dc-name">Zürich, Switzerland</div>
|
|
<div class="dc-sub">Capital of Privacy</div>
|
|
<div class="dc-status"><span class="dc-dot"></span>Headquarters</div>
|
|
<a href="/signup?region=eu-central-2" class="btn btn-gold btn-block">Buy now →</a>
|
|
</div>
|
|
<!-- Closest POP — populated by JS -->
|
|
<div id="closest-pop" class="dc-card" data-lon="999">
|
|
<div class="dc-icon">📍</div>
|
|
<div id="closest-name" class="dc-name">Nearest region</div>
|
|
<div id="closest-sub" class="dc-sub">Locating you…</div>
|
|
<div class="dc-status"><span class="dc-dot"></span>Closest to you</div>
|
|
<a id="closest-buy" href="/signup" class="btn btn-accent btn-block">Buy now →</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="divider">
|
|
|
|
<!-- Why encryption, not jurisdiction -->
|
|
<div class="section container">
|
|
<p class="label accent mb-3">Three-tier encryption</p>
|
|
<h2 class="mb-4">Jurisdiction is irrelevant.<br>Math is not.</h2>
|
|
<p class="lead mb-8">Your vault is encrypted at rest. Your credentials are encrypted per-field. Your identity fields are encrypted client-side with a key that never leaves your device. No server — ours or anyone's — can read what it doesn't have the key to. That's the real protection. Zürich is the belt to that suspenders: a jurisdiction where nobody will even try to force open what mathematics already guarantees they can't.</p>
|
|
<div class="grid-3">
|
|
<div class="card">
|
|
<p class="label mb-2">Vault Encryption</p>
|
|
<p>Entire vault encrypted at rest with AES-256-GCM. The baseline. Every password manager does this.</p>
|
|
</div>
|
|
<div class="card">
|
|
<p class="label accent mb-2">Credential Encryption</p>
|
|
<p>Per-field encryption. Your AI agent can read the API key it needs — but not the credit card number in the same entry.</p>
|
|
</div>
|
|
<div class="card red">
|
|
<p class="label red mb-2">Identity Encryption</p>
|
|
<p>Client-side. WebAuthn PRF. The key is derived from your WebAuthn authenticator — fingerprint, face, or hardware key — and never leaves your device. We cannot decrypt it. Period.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="divider">
|
|
|
|
<!-- What hosted adds -->
|
|
<div class="section container">
|
|
<p class="label accent mb-3">What hosted adds</p>
|
|
<h2 class="mb-8">Everything in self-hosted, plus</h2>
|
|
<div class="grid-3">
|
|
<div class="card alt">
|
|
<h3 class="mb-2">Managed infrastructure</h3>
|
|
<p>We run it, monitor it, and keep it up. You just use it.</p>
|
|
</div>
|
|
<div class="card alt">
|
|
<h3 class="mb-2">Daily encrypted backups</h3>
|
|
<p>Automatic daily backups. Encrypted at rest. Restorable on request.</p>
|
|
</div>
|
|
<div class="card alt">
|
|
<h3 class="mb-2">{{len .Pops}} regions</h3>
|
|
<p>Pick your region at signup. Your data stays there. Every continent covered.</p>
|
|
</div>
|
|
<div class="card alt">
|
|
<h3 class="mb-2">Automatic updates</h3>
|
|
<p>Security patches and new features deployed automatically. No downtime.</p>
|
|
</div>
|
|
<div class="card alt">
|
|
<h3 class="mb-2">TLS included</h3>
|
|
<p>HTTPS out of the box. No Caddy, no certbot, no renewal headaches.</p>
|
|
</div>
|
|
<div class="card alt">
|
|
<h3 class="mb-2">Email support</h3>
|
|
<p>Real human support. Not a chatbot. Not a forum post into the void.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="divider">
|
|
|
|
<!-- Backup strategy -->
|
|
<div class="section container">
|
|
<p class="label accent mb-3">Disaster recovery</p>
|
|
<h2 class="mb-4">Your backup is on the other side of the world.</h2>
|
|
<p class="lead mb-8">Every vault is automatically replicated to an inland backup location — no coastline, no tsunami risk, no storm surge. The two backup sites are Calgary and Zürich: different continents, different jurisdictions, maximum geographic separation. Your data is encrypted end-to-end — nobody but you can read it.</p>
|
|
<table class="data-table mb-4">
|
|
<thead>
|
|
<tr><th>Your region</th><th>Backup location</th><th>Why</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
{{range .Pops}}{{if or (eq .Status "live") (eq .City "Dubai") (eq .City "Bogotá") (eq .City "Almaty") (eq .City "Istanbul")}}
|
|
{{if eq .City "Zürich"}}<tr><td>{{.City}}</td><td>Calgary, Canada</td><td>Opposite continent, inland</td></tr>
|
|
{{else if eq .City "Calgary"}}<tr><td>{{.City}}</td><td>Zürich, Switzerland</td><td>Opposite continent, inland</td></tr>
|
|
{{else if or (eq .Country "JP") (eq .Country "KR") (eq .Country "AU") (eq .Country "SG") (eq .Country "IN") (eq .Country "HK") (eq .Country "ZA") (eq .Country "NG") (eq .Country "SE") (eq .Country "AE") (eq .Country "TR") (eq .Country "KZ")}}<tr><td>{{.City}}{{if ne .Status "live"}} <span class="text-tertiary text-sm">(planned)</span>{{end}}</td><td>Calgary, Canada</td><td>Farthest inland site</td></tr>
|
|
{{else}}<tr><td>{{.City}}{{if ne .Status "live"}} <span class="text-tertiary text-sm">(planned)</span>{{end}}</td><td>Zürich, Switzerland</td><td>Farthest inland site</td></tr>
|
|
{{end}}{{end}}{{end}}
|
|
</tbody>
|
|
</table>
|
|
<div class="card" style="margin-top:24px;border-left:3px solid var(--brand-red)">
|
|
<p class="label red mb-2">Dubai (me-central-1) — temporarily unavailable</p>
|
|
<p class="mb-3">On March 1, 2026, drone strikes physically damaged two of three AWS availability zones in the UAE. Both AWS Middle East regions (UAE and Bahrain) remain offline. We are waiting for AWS to restore service before we can offer Dubai as a region again. No customer data was affected — this is exactly why we replicate every vault to an inland backup site on the other side of the world.</p>
|
|
<p>If you were planning to use Dubai, the nearest alternatives are <strong>Mumbai</strong>, <strong>Istanbul</strong>, and <strong>Almaty</strong>. If you need to transfer an existing vault to a different region, we will do it for free — contact <a href="mailto:support@clavitor.ai">support@clavitor.ai</a>.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="divider">
|
|
|
|
<!-- CTA -->
|
|
<div class="section container">
|
|
<h2 class="mb-4">Ready?</h2>
|
|
<p class="lead mb-6"><s>$20</s> $12/yr. 7-day money-back. Every feature included.</p>
|
|
<div class="btn-row">
|
|
<a href="/signup" class="btn btn-primary">Get started</a>
|
|
<a href="/pricing" class="btn btn-ghost">Compare plans →</a>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{define "hosted-script"}}
|
|
<script>
|
|
(function() {
|
|
const W = 1000, H = 460;
|
|
const ns = 'http://www.w3.org/2000/svg';
|
|
|
|
function project(lon, lat) {
|
|
const latR = Math.min(Math.abs(lat), 85) * Math.PI / 180 * (lat < 0 ? -1 : 1);
|
|
const miller = 1.25 * Math.log(Math.tan(Math.PI/4 + 0.4*latR));
|
|
const maxMiller = 1.25 * Math.log(Math.tan(Math.PI/4 + 0.4*80*Math.PI/180));
|
|
const x = (lon + 180) / 360 * W;
|
|
const y = H/2 - (miller / (2*maxMiller)) * H;
|
|
return [Math.round(x*10)/10, Math.round(y*10)/10];
|
|
}
|
|
|
|
function addPopDot(svg, pop, delay) {
|
|
const [x, y] = project(pop.lon, pop.lat);
|
|
const isHQ = pop.city === 'Zürich';
|
|
const isLive = pop.status === 'live';
|
|
const isDubai = pop.city === 'Dubai';
|
|
const dotColor = isDubai ? '#ca8a04' : isHQ ? '#0A0A0A' : isLive ? '#DC2626' : '#F5B7B7';
|
|
const textColor = isDubai ? '#ca8a04' : isHQ ? '#0A0A0A' : isLive ? '#B91C1C' : '#777777';
|
|
const pulseColor = isHQ ? '#0A0A0A' : isLive ? '#DC2626' : '#F5B7B7';
|
|
const dotSize = isHQ ? 11 : 9;
|
|
const pulseR = isHQ ? 5 : 4;
|
|
const pulseMax = isHQ ? 18 : 13;
|
|
const tooltip = isDubai ? 'Dubai · Temporarily unavailable — AWS me-central-1 damaged by drone strikes (March 2026)' : isLive ? pop.city + ' · Live' : pop.city + ' · Planned';
|
|
|
|
// Pulse rings — only for live/HQ pops
|
|
let r1, r2;
|
|
if (isLive || isHQ) {
|
|
r1 = document.createElementNS(ns, 'circle');
|
|
r1.setAttribute('cx', x); r1.setAttribute('cy', y);
|
|
r1.setAttribute('r', pulseR); r1.setAttribute('fill', 'none');
|
|
r1.setAttribute('stroke', pulseColor); r1.setAttribute('stroke-width', isHQ ? '2' : '1.5');
|
|
const a1 = document.createElementNS(ns, 'animate');
|
|
a1.setAttribute('attributeName', 'r'); a1.setAttribute('values', pulseR+';'+pulseMax+';'+pulseR);
|
|
a1.setAttribute('dur', '2.4s'); a1.setAttribute('begin', delay+'s'); a1.setAttribute('repeatCount', 'indefinite');
|
|
const a2 = document.createElementNS(ns, 'animate');
|
|
a2.setAttribute('attributeName', 'stroke-opacity'); a2.setAttribute('values', '0.6;0;0.6');
|
|
a2.setAttribute('dur', '2.4s'); a2.setAttribute('begin', delay+'s'); a2.setAttribute('repeatCount', 'indefinite');
|
|
r1.appendChild(a1); r1.appendChild(a2);
|
|
|
|
r2 = document.createElementNS(ns, 'circle');
|
|
r2.setAttribute('cx', x); r2.setAttribute('cy', y);
|
|
r2.setAttribute('r', pulseR); r2.setAttribute('fill', 'none');
|
|
r2.setAttribute('stroke', pulseColor); r2.setAttribute('stroke-width', isHQ ? '1.5' : '1');
|
|
const a3 = document.createElementNS(ns, 'animate');
|
|
a3.setAttribute('attributeName', 'r'); a3.setAttribute('values', pulseR+';'+pulseMax+';'+pulseR);
|
|
a3.setAttribute('dur', '2.4s'); a3.setAttribute('begin', (delay+0.8)+'s'); a3.setAttribute('repeatCount', 'indefinite');
|
|
const a4 = document.createElementNS(ns, 'animate');
|
|
a4.setAttribute('attributeName', 'stroke-opacity'); a4.setAttribute('values', '0.4;0;0.4');
|
|
a4.setAttribute('dur', '2.4s'); a4.setAttribute('begin', (delay+0.8)+'s'); a4.setAttribute('repeatCount', 'indefinite');
|
|
r2.appendChild(a3); r2.appendChild(a4);
|
|
}
|
|
|
|
// Square dot
|
|
const half = dotSize / 2;
|
|
const dot = document.createElementNS(ns, 'rect');
|
|
dot.setAttribute('x', x - half); dot.setAttribute('y', y - half);
|
|
dot.setAttribute('width', dotSize); dot.setAttribute('height', dotSize);
|
|
dot.setAttribute('fill', dotColor); dot.setAttribute('stroke', '#F5F5F5'); dot.setAttribute('stroke-width', '1.5');
|
|
const title = document.createElementNS(ns, 'title');
|
|
title.textContent = tooltip;
|
|
dot.appendChild(title);
|
|
|
|
// Label
|
|
const label = document.createElementNS(ns, 'text');
|
|
label.setAttribute('x', x); label.setAttribute('y', y - half - 4);
|
|
label.setAttribute('font-family', 'Inter,sans-serif');
|
|
label.setAttribute('font-size', '8.5');
|
|
label.setAttribute('fill', textColor);
|
|
label.setAttribute('text-anchor', 'middle');
|
|
label.setAttribute('opacity', '0.85');
|
|
label.textContent = pop.city;
|
|
const labelTitle = document.createElementNS(ns, 'title');
|
|
labelTitle.textContent = tooltip;
|
|
label.appendChild(labelTitle);
|
|
|
|
if (r1) svg.appendChild(r1);
|
|
if (r2) svg.appendChild(r2);
|
|
svg.appendChild(dot);
|
|
svg.appendChild(label);
|
|
}
|
|
|
|
function addVisitorDot(lat, lon, city) {
|
|
const svg = document.getElementById('worldmap');
|
|
if (!svg) return;
|
|
const [x, y] = project(lon, lat);
|
|
|
|
const ring = document.createElementNS(ns, 'circle');
|
|
ring.setAttribute('cx', x); ring.setAttribute('cy', y);
|
|
ring.setAttribute('r', '3'); ring.setAttribute('fill', 'none');
|
|
ring.setAttribute('stroke', '#0A0A0A'); ring.setAttribute('stroke-width', '1.5');
|
|
const a1 = document.createElementNS(ns, 'animate');
|
|
a1.setAttribute('attributeName', 'r'); a1.setAttribute('values', '3;16;3');
|
|
a1.setAttribute('dur', '2s'); a1.setAttribute('repeatCount', 'indefinite');
|
|
const a2 = document.createElementNS(ns, 'animate');
|
|
a2.setAttribute('attributeName', 'stroke-opacity'); a2.setAttribute('values', '0.8;0;0.8');
|
|
a2.setAttribute('dur', '2s'); a2.setAttribute('repeatCount', 'indefinite');
|
|
ring.appendChild(a1); ring.appendChild(a2);
|
|
|
|
const dot = document.createElementNS(ns, 'circle');
|
|
dot.setAttribute('cx', x); dot.setAttribute('cy', y);
|
|
dot.setAttribute('r', '4'); dot.setAttribute('fill', '#0A0A0A');
|
|
dot.setAttribute('stroke', '#ffffff'); dot.setAttribute('stroke-width', '1.5');
|
|
|
|
const label = document.createElementNS(ns, 'text');
|
|
label.setAttribute('x', x); label.setAttribute('y', y + 15);
|
|
label.setAttribute('font-family', 'Inter,sans-serif');
|
|
label.setAttribute('font-size', '10');
|
|
label.setAttribute('fill', '#0A0A0A');
|
|
label.setAttribute('text-anchor', 'middle');
|
|
label.setAttribute('font-weight', '500');
|
|
label.textContent = city || 'You';
|
|
|
|
svg.appendChild(ring);
|
|
svg.appendChild(dot);
|
|
svg.appendChild(label);
|
|
}
|
|
|
|
const POPS = [{{range .Pops}}
|
|
{city:"{{.City}}", country:"{{.Country}}", region:"{{.RegionName}}", lat:{{.Lat}}, lon:{{.Lon}}, status:"{{.Status}}", provider:"{{.Provider}}"},{{end}}
|
|
];
|
|
|
|
// Render all POP dots from DB data
|
|
const svg = document.getElementById('worldmap');
|
|
if (svg) {
|
|
POPS.forEach((pop, i) => addPopDot(svg, pop, (i * 0.08).toFixed(2)));
|
|
}
|
|
|
|
// Backup route lines
|
|
const backupToCalgary = new Set(['JP','KR','AU','SG','IN','HK','ZA','NG','SE']);
|
|
function getBackupCity(pop) {
|
|
if (pop.city === 'Zürich') return 'Calgary';
|
|
if (pop.city === 'Calgary') return 'Zürich';
|
|
return backupToCalgary.has(pop.country) ? 'Calgary' : 'Zürich';
|
|
}
|
|
|
|
let backupLines = [];
|
|
let backupVisible = false;
|
|
const backupBtn = document.getElementById('backup-toggle');
|
|
if (backupBtn) {
|
|
backupBtn.addEventListener('click', function() {
|
|
backupVisible = !backupVisible;
|
|
if (backupVisible) {
|
|
backupBtn.textContent = 'Hide backup routes';
|
|
const livePops = POPS.filter(p => p.status === 'live');
|
|
livePops.forEach(pop => {
|
|
const backupCity = getBackupCity(pop);
|
|
const target = livePops.find(p => p.city === backupCity);
|
|
if (!target || target.city === pop.city) return;
|
|
const [x1, y1] = project(pop.lon, pop.lat);
|
|
const [x2, y2] = project(target.lon, target.lat);
|
|
const line = document.createElementNS(ns, 'line');
|
|
line.setAttribute('x1', x1); line.setAttribute('y1', y1);
|
|
line.setAttribute('x2', x2); line.setAttribute('y2', y2);
|
|
line.setAttribute('stroke', '#DC2626'); line.setAttribute('stroke-width', '0.8');
|
|
line.setAttribute('stroke-opacity', '0.3'); line.setAttribute('stroke-dasharray', '4 3');
|
|
svg.insertBefore(line, svg.querySelector('rect'));
|
|
backupLines.push(line);
|
|
});
|
|
} else {
|
|
backupBtn.textContent = 'Show backup routes';
|
|
backupLines.forEach(l => l.remove());
|
|
backupLines = [];
|
|
}
|
|
});
|
|
}
|
|
|
|
function findClosestPop(lat, lon) {
|
|
return POPS.reduce((best, p) => {
|
|
const d = (lat-p.lat)**2 + (lon-p.lon)**2;
|
|
const bd = (lat-best.lat)**2 + (lon-best.lon)**2;
|
|
return d < bd ? p : best;
|
|
});
|
|
}
|
|
|
|
function handleGeoData(d) {
|
|
if (!d.latitude || !d.longitude) return;
|
|
addVisitorDot(d.latitude, d.longitude, d.city || 'You');
|
|
|
|
const closest = findClosestPop(d.latitude, d.longitude);
|
|
const nameEl = document.getElementById('closest-name');
|
|
const subEl = document.getElementById('closest-sub');
|
|
const buyEl = document.getElementById('closest-buy');
|
|
if (nameEl) nameEl.textContent = closest.city;
|
|
if (subEl) subEl.textContent = d.city ? `~${d.city}` : 'Your region';
|
|
if (buyEl) buyEl.href = `/signup?region=${closest.region}`;
|
|
}
|
|
|
|
// Ask browser geolocation first (accurate, triggers permission prompt)
|
|
// Fall back to server-side IP lookup if denied or unavailable
|
|
function tryIPGeo() {
|
|
fetch('/geo')
|
|
.then(r => r.json())
|
|
.then(d => { if (d.latitude) handleGeoData(d); })
|
|
.catch(() => {});
|
|
}
|
|
|
|
if (navigator.geolocation) {
|
|
navigator.geolocation.getCurrentPosition(
|
|
pos => {
|
|
handleGeoData({
|
|
latitude: pos.coords.latitude,
|
|
longitude: pos.coords.longitude,
|
|
city: '', region: '', country_name: '', country_code: ''
|
|
});
|
|
},
|
|
() => tryIPGeo() // denied — fall back to IP
|
|
);
|
|
} else {
|
|
tryIPGeo();
|
|
}
|
|
})();
|
|
</script>
|
|
{{end}}
|