1 2<!DOCTYPE html> 3<html lang="en"> 4<head> 5 <meta charset="UTF-8"> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 <link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin> 8 9 10 11 <title>Compliance for Swiss Software Vendors | Application Catalog</title> 12 <meta name="description" content="Your customers audit you, so they audit us. Datacenters, ownership, subprocessors, DPA under DSG Art. 9, tenant isolation and audit rights, answered here."> 13 <link rel="icon" type="image/svg+xml" href="/static/appcat_ch/favicon.326f623176ef.svg"> 14 15 16 17 18 19 20 21 22 23 <link rel="canonical" href="https://www.appcat.ch/compliance/"> 24 25 26 27 28 29 30 31 32 33 <link rel="alternate" hreflang="en" href="https://www.appcat.ch/compliance/"> 34 35 <link rel="alternate" hreflang="de" href="https://www.appcat.ch/de/compliance/"> 36 37 38 <link rel="alternate" hreflang="x-default" href="https://www.appcat.ch/compliance/"> 39 40 41 <meta property="og:type" content="website"> 42 <meta property="og:url" content="https://www.appcat.ch/compliance/"> 43 <meta property="og:title" content="Compliance for Swiss Software Vendors | Application Catalog"> 44 <meta property="og:description" content="Your customers audit you, so they audit us. Datacenters, ownership, subprocessors, DPA under DSG Art. 9, tenant isolation and audit rights, answered here."> 45 46 47 48 <meta property="og:image" content="https://www.appcat.ch/static/appcat_ch/logo.381ee741974f.svg"> 49 <meta name="twitter:card" content="summary"> 50 <meta name="twitter:title" content="Compliance for Swiss Software Vendors | Application Catalog"> 51 <meta name="twitter:description" content="Your customers audit you, so they audit us. Datacenters, ownership, subprocessors, DPA under DSG Art. 9, tenant isolation and audit rights, answered here."> 52 <meta name="twitter:image" content="https://www.appcat.ch/static/appcat_ch/logo.381ee741974f.svg"> 53 54 55
55<script type="application/ld+json"> 56{ 57 "@context": "https://schema.org", 58 "@type": "Organization", 59 "name": "VSHN Application Catalog", 60 "legalName": "VSHN AG", 61 "description": "VSHN Application Catalog: managed databases, identity, and platform services on your private cloud. One contract, one ops team, Swiss operations, ISO 27001.", 62 "url": "https://www.appcat.ch/", 63 "logo": "https://www.appcat.ch/static/appcat_ch/logo.381ee741974f.svg", 64 "email": "[email protected]", 65 "telephone": "+41 44 545 53 00", 66 "address": { 67 "@type": "PostalAddress", 68 "streetAddress": "Neugasse 10", 69 "postalCode": "8005", 70 "addressLocality": "Zürich", 71 "addressCountry": "CH" 72 }, 73 "contactPoint": [ 74 { 75 "@type": "ContactPoint", 76 "contactType": "sales", 77 "areaServed": "CH", 78 "email": "[email protected]", 79 "telephone": "+41 44 545 53 00", 80 "availableLanguage": [ 81 "en", 82 "de" 83 ] 84 } 85 ], 86 "areaServed": "CH", 87 "sameAs": [ 88 "https://mastodon.vshn.social/@vshn", 89 "https://bsky.app/profile/vshnch.bsky.social", 90 "https://www.linkedin.com/company/vshn-ag", 91 "https://github.com/vshn", 92 "https://gitlab.com/vshn", 93 "https://www.facebook.com/vshn.ch/", 94 "https://www.instagram.com/vshn_ch/", 95 "https://vshn.tv/", 96 "https://www.vshn.ch/" 97 ] 98} 99</script>
99 100
101<script type="application/ld+json"> 102{ 103 "@context": "https://schema.org", 104 "@type": "BreadcrumbList", 105 "itemListElement": [ 106 {"@type": "ListItem", "position": 1, "name": "VSHN Application Catalog", "item": "https://www.appcat.ch/"}, 107 {"@type": "ListItem", "position": 2, "name": "Compliance for Swiss Software Vendors | Application Catalog"} 108 ] 109} 110</script>
110 111 112 113 114 115 116 117 118 119
119<script async src="/js/pa-KoItj0LFVvVZ3udfs6o1x.js"></script>
119 120
120<script> 121 window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}}; 122 plausible.init({endpoint: "/api/event"}) 123 </script>
123 124 125 126
126<script> 127 window.dataLayer = window.dataLayer || []; 128 function gtag(){dataLayer.push(arguments);} 129 // Belt and braces with the consent gate on loadGtag() below: if a future 130 // load path escapes the gate, the click identifier is still kept out of 131 // the request. Set before the consent call, because the flag applies to 132 // what is already queued. 133 gtag('set', 'ads_data_redaction', true); 134 gtag('consent', 'default', { 135 'analytics_storage': 'denied', 136 'ad_storage': 'denied', 137 'ad_user_data': 'denied', 138 'ad_personalization': 'denied', 139 'wait_for_update': 500 140 }); 141 gtag('js', new Date()); 142 gtag('config', 'G-FWQPMNS0R2'); 143 144 gtag('config', 'AW-18008867378', {'allow_enhanced_conversions': true}); 145 146 // gtag.js loads only once the visitor has accepted cookies (#925). 147 // 148 // Consent Mode's denied defaults stop the cookies, not the request. The 149 // tag used to load on the first scroll or click, and immediately for a 150 // visitor arriving from an ad, so Google was called with the time, user 151 // agent, referrer, click identifier and IP before the banner was 152 // answered. VSHN's CISO asked for the data-minimising option in 153 // vshn/docs/products MR 355. The cost is Google Ads' modelled conversions 154 // for visitors who never accept, which is a deliberate trade. 155 // 156 // Every caller goes through here, the form handlers in _contact.html and 157 // _subscribe.html included, so this one check covers all of them. Their 158 // gtag() calls still queue in dataLayer and are simply never sent. 159 window._gtag_loaded = false; 160 function loadGtag() { 161 if (window._gtag_loaded) return; 162 var consented = false; 163 try { consented = localStorage.getItem('cookie_consent') === 'granted'; } catch(e) {} 164 if (!consented) return; 165 window._gtag_loaded = true; 166 167 var s = document.createElement('script'); 168 s.async = true; 169 s.src = 'https://www.googletagmanager.com/gtag/js?id=G-FWQPMNS0R2'; 170 document.head.appendChild(s); 171 172 173 } 174 // Capture gclid + UTM params and propagate them across our sites. 175 // 176 // Nothing is written to the device before cookie consent (#852): ePrivacy 177 // Art. 5(3) covers any storage on the device, local storage included, and 178 // attribution is not strictly necessary. This page's values are held in 179 // memory, which is enough for a form on this page and for links leaving it. 180 // They reach localStorage only through vshnPersistAttribution, which runs 181 // on load for a returning consent and when the banner is accepted. 182 (function() { 183 var params = new URLSearchParams(location.search); 184 var consented = false; 185 try { consented = localStorage.getItem('cookie_consent') === 'granted'; } catch(e) {} 186 var pending = {}; 187 var m = location.search.match(/[?&]gclid=([^&]+)/); 188 if (m) { 189 pending.gclid = m[1]; 190 } 191 // The list comes from core.attribution.UTM_KEYS so it cannot drift from 192 // what core/ads.py puts on the click URL (#556). 193 var utmKeys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term']; 194 var newTouch = utmKeys.some(function(key) { return params.get(key); }); 195 utmKeys.forEach(function(key) { 196 var val = params.get(key); 197 if (val) pending[key] = val; 198 }); 199 if (params.get('ref')) pending.ref = params.get('ref'); 200 if (document.referrer) pending.referrer = document.referrer; 201 202 window.vshnPersistAttribution = function() { 203 try { 204 if (pending.gclid) localStorage.setItem('gclid', pending.gclid); 205 // A new campaign touch replaces the whole set, so a key the new source 206 // does not send (LinkedIn sends no utm_term) cannot survive from the old one. 207 if (newTouch) utmKeys.forEach(function(key) { localStorage.removeItem(key); });
208 utmKeys.forEach(function(key) { if (pending[key]) localStorage.setItem(key, pending[key]); }); 209 if (pending.ref) localStorage.setItem('ref', pending.ref); 210 // First referrer only: the one that brought the visitor, not an internal hop. 211 if (pending.referrer && !localStorage.getItem('referrer')) localStorage.setItem('referrer', pending.referrer); 212 } catch(e) {} 213 }; 214 if (consented) window.vshnPersistAttribution(); 215 216 // One reader for every attribution value: this page's first, then what an 217 // earlier consented visit stored. The referrer reads the other way round, 218 // because the first one is the one that counts. 219 window.vshnAttr = function(key) { 220 var stored = null; 221 try { stored = localStorage.getItem(key); } catch(e) {} 222 if (key === 'referrer') return stored || pending.referrer || null; 223 return pending[key] || stored; 224 }; 225 // Build list of own domains from footer product links 226 var ownDomains = [ 227 'https://www.vshn.ch/',
228 'https://www.appuio.ch', 229 'https://www.servala.com', 230 'https://k8up.io', 231 'https://codey.ch', 232 'https://syn.tools', 233 'https://espejote.io', 234 'https://www.gitlab.ch', 235 'https://www.forgejo.ch', 236 'https://www.managed-keycloak.ch', 237 'https://www.managed-redis.ch', 238 'https://www.managed-rabbitmq.ch', 239 'https://www.enterprisedb.ch', 240 'https://www.managed-postgresql.ch', 241 'https://www.managed-mariadb.ch', 242 'https://www.the-devops-company.ch', 243 'https://www.cloudnativealliance.ch', 244 'https://www.cloud-native.ch', 245 'https://www.llmops.ch', 246 'https://www.varnish.ch', 247 'https://www.cnc-meetup.ch', 248 'https://www.openbao.ch', 249 'https://www.nginx-hosting.net', 250 'https://www.swiss-cdn.ch', 251 'https://www.vllm.ch', 252 'https://www.litellm.ch', 253 'https://www.managed-openshift.ch', 254 'https://www.kubernetes-experten.ch', 255 'https://www.llm-d.ch', 256 'https://www.appcat.ch', 257 'https://www.managed-kubernetes.ch', 258 'https://ocre.vshn.ch', 259 'https://www.application-operations.ch', 260 'https://www.developer-platform.ch' 261 262 ]; 263 // Add domains not in the footer product list 264 ownDomains.push('https://www.vshn.net'); 265 // Extract base domains for suffix matching (covers subdomains like portal.servala.com) 266 var ownBaseDomains = []; 267 ownDomains.forEach(function(u) { 268 try { 269 var h = new URL(u).hostname; 270 // Strip leading www. to get the base domain 271 ownBaseDomains.push(h.replace(/^www\./, '')); 272 } catch(e) {} 273 }); 274 275 // Annotate cross-site links with tracking params 276 document.addEventListener('click', function(e) { 277 var a = e.target.closest('a[href]'); 278 if (!a) return; 279 try { 280 var url = new URL(a.href, location.origin); 281 if (url.protocol !== 'https:' && url.protocol !== 'http:') return; 282 // Check if link points to an own domain (or subdomain) or is a booking button 283 var linkHost = url.hostname; 284 var isOwn = ownBaseDomains.some(function(d) { 285 return linkHost === d || linkHost.endsWith('.' + d); 286 }); 287 var isBooking = a.classList.contains('booking-btn'); 288 if (!isOwn && !isBooking) return; 289 // Skip same-site links (only add ref for cross-site navigation) 290 var currentBase = location.hostname.replace(/^www\./, ''); 291 if (linkHost === currentBase || linkHost === 'www.' + currentBase) return; 292 // Append tracking params (don't overwrite existing) 293 // localStorage is per-origin, so the URL is the only way UTM values 294 // reach another one of our domains. 295 var trackingParams = {'gclid': null, 'ref': 'appcat_ch'};
296 utmKeys.forEach(function(key) { trackingParams[key] = null; }); 297 Object.keys(trackingParams).forEach(function(key) { 298 if (url.searchParams.has(key)) return; 299 var val = trackingParams[key] || window.vshnAttr(key); 300 if (val) url.searchParams.set(key, val); 301 }); 302 a.href = url.toString(); 303 } catch(err) {} 304 }); 305 306 // Fill a form's hidden attribution fields through vshnAttr. Shared by the 307 // contact and subscribe forms; selects by name rather than by id, because 308 // codey.ch renders both on one page. 309 window.vshnFillAttribution = function(form) { 310 function fill(name, val) { 311 if (!val) return; 312 var field = form.querySelector('[name="' + name + '"]'); 313 if (field) field.value = val; 314 } 315 fill('gclid', window.vshnAttr('gclid')); 316 utmKeys.forEach(function(key) { fill(key, window.vshnAttr(key)); }); 317 // The cookie choice decides whether the server may upload the 318 // conversion to LinkedIn (#925). Filled here so both forms carry it. 319 try { fill('cookie_consent', localStorage.getItem('cookie_consent')); } catch(e) {} 320 }; 321 322 // Propagate ?site= to relative links for preview deployments 323 var siteParam = params.get('site'); 324 if (siteParam) { 325 document.addEventListener('click', function(e) { 326 var a = e.target.closest('a[href]'); 327 if (!a) return; 328 try { 329 var href = a.getAttribute('href'); 330 if (!href || href.charAt(0) !== '/') return; 331 var url = new URL(a.href); 332 if (url.origin !== location.origin) return; 333 if (!url.searchParams.has('site')) { 334 url.searchParams.set('site', siteParam); 335 a.href = url.toString(); 336 } 337 } catch(err) {} 338 }); 339 } 340 })(); 341 </script>
341 342
343<script> 344(function () { 345 var SRC = 'https://cdn.jsdelivr.net/npm/@cap.js/[email protected]'; 346 var script = null; 347 var solved = new WeakMap(); 348 349 function loadWidgetScript() { 350 if (script) return script; 351 script = new Promise(function (resolve, reject) { 352 var el = document.createElement('script'); 353 el.src = SRC; 354 el.onload = resolve; 355 el.onerror = function () { 356 /* Cleared so a later attempt can retry rather than reusing a 357 permanently rejected promise. */ 358 script = null; 359 reject(new Error('cap widget failed to load')); 360 }; 361 document.head.appendChild(el); 362 }); 363 return script; 364 } 365 366 /* Mount the widget into this form's .cap-row, once. Returns a promise that 367 resolves with the token when the visitor solves the challenge. Callers 368 that only want the widget on screen can ignore the promise. */ 369 window.vshnCapMount = function (form) { 370 var row = form.querySelector('.cap-row'); 371 if (!row) return Promise.resolve(null); 372 if (solved.has(row)) return solved.get(row); 373 374 var pending = loadWidgetScript().then(function () { 375 return new Promise(function (resolve) { 376 var widget = document.createElement('cap-widget'); 377 /* Every data-cap-* attribute the template put on the row, so the 378 endpoint and the translated labels reach the widget and the two 379 lists cannot drift apart. */ 380 Array.prototype.forEach.call(row.attributes, function (attr) { 381 if (attr.name.indexOf('data-cap-') === 0) { 382 widget.setAttribute(attr.name, attr.value); 383 } 384 }); 385 widget.addEventListener('solve', function (e) { 386 var input = form.querySelector('input[name="cap-token"]'); 387 if (!input) { 388 input = document.createElement('input'); 389 input.type = 'hidden'; 390 input.name = 'cap-token'; 391 form.appendChild(input); 392 } 393 /* Assigned on every solve, not just the first: tokens expire, and 394 the widget re-solves. */ 395 input.value = e.detail.token; 396 resolve(e.detail.token); 397 }); 398 row.appendChild(widget); 399 }); 400 }); 401 solved.set(row, pending); 402 return pending; 403 }; 404 405 /* Has this form got a token to send? Checked at submit rather than waiting 406 on the promise, because an unsolved challenge would otherwise hang the 407 submit forever behind a disabled button. */ 408 window.vshnCapReady = function (form) { 409 if (!form.querySelector('.cap-row')) return true; 410 var input = form.querySelector('input[name="cap-token"]'); 411 return !!(input && input.value); 412 }; 413 414 /* Mount on the first interaction with any form on the page. focusin rather 415 than an IntersectionObserver on purpose: Lighthouse scrolls the page 416 while auditing, which would mount the widget and put the anch
416or back. */ 417 document.addEventListener('focusin', function (e) { 418 var form = e.target.closest && e.target.closest('form'); 419 if (form && form.querySelector('.cap-row')) { 420 window.vshnCapMount(form); 421 } 422 }); 423})(); 424</script>
424 425 426 427 428 <style> 429 html { scroll-behavior: smooth; } 430 #CookieConsent { 431 position: fixed; bottom: 0; left: 0; right: 0; z-index: 9999; 432 background: #1a1a1a; color: #fff; padding: 1rem 1.5rem; 433 display: flex; align-items: center; justify-content: center; gap: 1.5rem; 434 font-size: 0.9rem; 435 } 436 #CookieConsent a { color: var(--color-primary, #4CC3FF); } 437 #CookieConsent button { 438 padding: 0.5rem 1.25rem; border: none; border-radius: 4px; 439 cursor: pointer; font-size: 0.9rem; 440 } 441 #CookieConsentAccepted { background: var(--color-primary, #FFBF00); color: var(--color-on-primary, #000); font-weight: 600; } 442 #CookieConsentDeclined { background: transparent; color: #ccc; border: 1px solid #555; } 443 @media (max-width: 600px) { 444 #CookieConsent { flex-direction: column; text-align: center; gap: 0.75rem; } 445 } 446 </style> 447 448 <style> 449 .lang-switcher { 450 display: flex; 451 gap: 0.5rem; 452 align-items: center; 453 margin-left: 1rem; 454 } 455 .lang-switcher a { 456 color: rgba(255, 255, 255, 0.7); 457 text-decoration: none; 458 font-size: 0.85rem; 459 font-weight: 600; 460 padding: 0.25rem 0.5rem; 461 border-radius: 4px; 462 } 463 .lang-switcher a:hover { 464 color: #fff; 465 } 466 .lang-switcher a.active { 467 background: var(--color-primary, #333); 468 color: #fff; 469 } 470 @media (max-width: 1280px) { 471 .lang-switcher { 472 margin-left: 0; 473 margin-top: 0.75rem; 474 padding-top: 0.75rem; 475 border-top: 1px solid rgba(255, 255, 255, 0.1); 476 } 477 } 478 </style> 479 480 <style> 481 footer { 482 background: var(--color-dark, #002a3a); 483 color: #aaa; 484 font-size: 0.9rem; 485 } 486 .footer-body { 487 display: flex; flex-wrap: wrap; gap: 3rem; 488 padding: 3rem 2rem; max-width: 1000px; margin: 0 auto; 489 } 490 .footer-about, .footer-connect { flex: 1; min-width: 240px; } 491 footer h3 { color: #fff; margin: 0 0 1rem; font-size: 1.1rem; } 492 .footer-logo { height: 32px; width: auto; filter: brightness(0) invert(1); margin-bottom: 1rem; display: block; } 493 .footer-contact-list { list-style: none; padding: 0; margin: 0 0 1rem; line-height: 2; } 494 .footer-contact-list a { color: #5bc4fa; text-decoration: none; } 495 .footer-social { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 0.75rem; } 496 .footer-social a { color: #aaa; text-decoration: none; display: inline-flex; align-items: center; } 497 .footer-social a svg { width: 22px; height: 22px; } 498 .footer-social a:hover { color: #fff; } 499 .footer-disclaimer { text-align: center; padding: 0.75rem 2rem; color: #999; font-size: 0.85rem; } 500 .footer-products { text-align: center; padding: 1.5rem 2rem; border-top: 1px solid #333; } 501 .footer-products-heading { color: #888; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em; margin: 0 0 1rem; } 502 .footer-products-list { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; justify-content: center; gap: 0.75rem; } 503 .footer-products-list a, .footer-products-list .product-active { display: block; opacity: 0.65; transition: opacity 0.2s; } 504 .footer-products-list a:hover, .footer-products-list .product-active { opacity: 1; } 505 .footer-products-list .product-active { outline: 2px solid rgba(255,255,255,0.4); outline-offset: 3px; border-radius: 4px; } 506 .footer-products-list img { display: block; width: 32px; height: 32px; } 507 .footer-copy { text-align: center; border-top: 1px solid #333; padding: 1rem 2rem; color: #999; } 508 footer a { color: var(--color-primary, #4CC3FF); text-decoration: none; } 509 footer a:hover { text-decoration: underline; } 510 </style> 511 512 <link rel="alternate" type="text/markdown" href="/compliance.md" title="This page as markdown"> 513 514<style> 515 :root { 516 --color-primary: #6983ff; 517 --color-dark: #002a3a; 518 --color-accent: #2d2471; 519 --color-primary-hover: #5570ee; 520 --color-on-primary: #fff; 521 } 522 523 524 * { box-sizing: border-box; } 525 526 body { 527 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 528 margin: 0; 529 color: #333; 530 line-height: 1.6; 531 } 532 533 nav { 534 background: var(--color-dark); 535 padding: 1rem 2rem; 536 display: flex; 537 align-items: center; 538 gap: 1.5rem; 539 flex-wrap: nowrap; 540 } 541 nav > a { color: #fff; text-decoration: none; font-weight: bold; font-size: 1.1rem; } 542 .nav-logo { height: 44px; width: auto; display: block; } 543 nav > a { transition: transform 0.2s, filter 0.2s; } 544 nav > a:hover { 545 transform: translateY(-2px); 546 filter: drop-shadow(0 0 10px color-mix(in srgb, var(--color-primary) 55%, transparent)); 547 } 548 .nav-toggle { 549 display: none; 550 background: none; border: none; color: #fff; font-size: 1.5rem; 551 cursor: pointer; padding: 0.25rem; margin-left: auto; 552 } 553 .nav-menu { display: flex; align-items: center; gap: 1.5rem; margin-left: auto; } 554 nav ul { list-style: none; margin: 0; padding: 0; display: flex; gap: 1.25rem; } 555 nav ul a { color: #ccc; text-decoration: none; font-size: 0.9rem; } 556 nav ul a:hover { color: #fff; } 557 .nav-cta { padding: 0.5rem 1.2rem; font-size: 0.9rem; } 558 559 @media (max-width: 1280px) { 560 nav { flex-wrap: wrap; } 561 .nav-toggle { display: block; } 562 .nav-menu { 563 display: none; 564 flex-basis: 100%; 565 flex-direction: column; 566 align-items: stretch; 567 gap: 0; 568 padding: 1rem 0 0.5rem; 569 } 570 .nav-menu--open { display: flex; } 571 nav ul { flex-direction: column; gap: 0; } 572 nav ul li a { display: block; padding: 0.75rem 0; min-height: 48px; } 573 .nav-cta { text-align: center; margin-top: 0.5rem; } 574 } 575 576 .btn { 577 background: var(--color-primary); 578 color: #fff; 579 padding: 0.8rem 2rem; 580 border-radius: 4px; 581 text-decoration: none; 582 font-weight: bold; 583 display: inline-block; 584 } 585 .btn:hover { background: var(--color-primary-hover); } 586 587 .prose { 588 max-width: 780px; 589 margin: 0 auto; 590 padding: 3rem 2rem 4rem; 591 font-size: 1.05rem; 592 line-height: 1.7; 593 } 594 .prose h1 { 595 font-size: 2.2rem; 596 font-weight: 900; 597 letter-spacing: -0.02em; 598 margin: 0 0 0.5rem; 599 color: var(--color-dark, #1a1a1a); 600 } 601 .prose h2 { 602 font-size: 1.5rem; 603 font-weight: 700; 604 margin: 2.5rem 0 1rem; 605 color: var(--color-dark, #1a1a1a); 606 border-bottom: 2px solid var(--color-primary, #eee); 607 padding-bottom: 0.3rem; 608 } 609 .prose h3 { 610 font-size: 1.2rem; 611 font-weight: 600; 612 margin: 2rem 0 0.75rem; 613 color: #444; 614 } 615 .prose p { margin: 0 0 1.25rem; } 616 .prose ul, .prose ol { margin: 0 0 1.25rem; padding-left: 1.5rem; } 617 .prose li { margin-bottom: 0.4rem; } 618 .prose a { color: var(--color-primary); text-decoration: underline; } 619 .prose a:hover { opacity: 0.8; } 620 .prose a.btn { color: #fff; text-decoration: none; } 621 .prose a.btn:hover { opacity: 1; } 622 .prose strong { font-weight: 600; } 623 .prose blockquote { 624 border-left: 4px solid var(--color-primary); 625 margin: 1.5rem 0; 626 padding: 0.75rem 1.25rem; 627 background: #f8f9fa; 628 color: #555; 629 } 630 .prose table { 631 width: 100%; 632 border-collapse: collapse; 633 margin: 1.5rem 0; 634 font-size: 0.95rem; 635 } 636 .prose th { 637 background: var(--color-dark, #1a1a1a); 638 color: #fff; 639 padding: 0.75rem 1rem; 640 text-align: left; 641 font-weight: 600; 642 } 643 .prose td { 644 padding: 0.65rem 1rem; 645 border-bottom: 1px solid #e5e7eb; 646 } 647 .prose tr:nth-child(even) { background: #f9fafb; } 648 .prose code { 649 background: #f1f3f5; 650 padding: 0.15rem 0.4rem; 651 border-radius: 3px; 652 font-size: 0.9em; 653 } 654 .prose .lead { 655 font-size: 1.15rem; 656 color: #555; 657 margin-bottom: 2rem; 658 } 659 660 .prose-cta { 661 background: var(--color-dark, #1a1a1a); 662 color: #fff; 663 padding: 2.5rem 2rem; 664 border-radius: 8px; 665 text-align: center; 666 margin: 3rem 0 1rem; 667 } 668 .prose-cta h2 { color: #fff; border: none; margin: 0 0 0.75rem; } 669 .prose-cta p { color: rgba(255,255,255,0.8); margin: 0 0 1.5rem; } 670 .prose-cta .btn { font-size: 1.05rem; padding: 0.9rem 2.2rem; border-radius: 8px; } 671 672 .contact { 673 background: var(--color-accent, #2d2471); 674 color: #fff; 675 text-align: center; 676 padding: 4rem 2rem; 677 } 678 .contact h2 { font-size: 2rem; margin: 0 0 1rem; color: #fff; border: none; } 679 .contact-intro { font-size: 1.1rem; opacity: 0.9; max-width: 560px; margin: 0 auto 2rem; } 680 .contact-intro a { color: #fff; text-decoration: underline; }
681 .contact-intro a:hover { opacity: 0.8; } 682 .contact .btn { background: var(--color-primary); border: none; cursor: pointer; font-size: 1rem; } 683 .contact .btn:hover { background: var(--color-primary-hover); } 684 .contact-form { max-width: 560px; margin: 0 auto; text-align: left; } 685 .form-row { margin-bottom: 1.2rem; } 686 .form-row label { display: block; font-size: 0.9rem; margin-bottom: 0.3rem; opacity: 0.85; } 687 .form-row input, .form-row textarea { 688 width: 100%; padding: 0.65rem 0.9rem; 689 border: 1px solid rgba(255,255,255,0.3); border-radius: 4px; 690 background: rgba(255,255,255,0.05); color: #fff; 691 font-size: 1rem; font-family: inherit; 692 } 693 .form-row input::placeholder, .form-row textarea::placeholder { opacity: 0.5; } 694 .form-row input:focus, .form-row textarea:focus { outline: none; border-color: var(--color-primary); background: rgba(255,255,255,0.15); } 695 .contact-success { background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.4); border-radius: 6px; padding: 1rem 1.5rem; max-width: 560px; margin: 0 auto; font-size: 1.1rem; } 696 .contact-error { background: rgba(220,53,69,0.3); border: 1px solid rgba(220,53,69,0.6); border-radius: 6px; padding: 0.8rem 1.2rem; max-width: 560px; margin: 0 auto 1.5rem; font-size: 0.95rem; } 697 .booking-btn { display: inline-block; font-size: 1.1rem; padding: 0.85rem 2.5rem; margin-bottom: 0; } 698 .contact-divider { max-width: 560px; margin: 1.5rem auto; opacity: 0.6; font-size: 0.95rem; } 699 .cap-row { display: flex; justify-content: center; } 700</style> 701 702</head> 703<body> 704 705 706<nav> 707 <a href="/"> 708 <img src="/static/appcat_ch/logo.381ee741974f.svg" alt="VSHN Application Catalog" class="nav-logo" width="220" height="44"> 709 710 </a> 711 <button class="nav-toggle" aria-label="Toggle menu" aria-expanded="false">☰</button> 712 <div class="nav-menu"> 713 714 <ul> 715 716 <li><a href="/#services">Services</a></li> 717 718 <li><a href="/#why-vshn">Why VSHN</a></li> 719 720 <li><a href="/#references">References</a></li> 721 722 <li><a href="/operating-model/">Operating model</a></li> 723 724 <li><a href="/compliance/">Compliance</a></li> 725 726 <li><a href="/sovereignty/">Sovereignty</a></li> 727 728 <li><a href="/partners/">Partners</a></li> 729 730 <li><a href="/#faq">FAQ</a></li> 731 732 <li><a href="https://docs.appcat.ch/" target="_blank" rel="noopener">Docs</a></li> 733 734 </ul> 735 736 737 <a href="/#contact" class="btn nav-cta">Book an Assessment</a> 738 739 740 <div class="lang-switcher"> 741 742 <a href="/compliance/" class="active">EN</a> 743
744 <a href="/de/compliance/">DE</a> 745 746 </div> 747 748 </div> 749</nav>
750<script> 751(function() { 752 var toggle = document.querySelector('.nav-toggle'); 753 var menu = document.querySelector('.nav-menu'); 754 toggle.addEventListener('click', function() { 755 var expanded = this.getAttribute('aria-expanded') === 'true'; 756 this.setAttribute('aria-expanded', !expanded); 757 menu.classList.toggle('nav-menu--open'); 758 }); 759 // Close mobile menu when clicking anchor links 760 document.querySelectorAll('a[href*="#"]').forEach(function(link) { 761 link.addEventListener('click', function() { 762 if (menu.classList.contains('nav-menu--open')) { 763 menu.classList.remove('nav-menu--open'); 764 toggle.setAttribute('aria-expanded', 'false'); 765 } 766 }); 767 }); 768})(); 769</script>
769 770 771 772 773 <main> 774 775 776 777 778 779<section id="content"> 780 <article class="prose"> 781 <h1>Compliance for software vendors: answering your customers' questions about us</h1> 782<p>When you sell software to insurers, banks, hospitals or cantons, their compliance team reviews your hosting provider before they sign. The questions arrive as a list, they are always roughly the same eight, and one of them is the difficult one: <em>can your provider grant audit rights to us?</em></p> 783<p>This page answers all eight, so you can forward it instead of writing the answers yourself.</p> 784<h2>The eight questions</h2> 785<h3>1. Where are your datacenters, physically?</h3> 786<p>Switzerland, and which Swiss datacenter depends on how you buy. VSHN operates none of its own.</p> 787<p><strong>Ordering through the portal (APPUiO Cloud).</strong> Two zones, both Swiss, and you pick one when you order: Cloudscale in <strong>Lupfig (AG)</strong> or Exoscale in <strong>Geneva (GE)</strong>. That is the whole list, and you can check it without an account on the <a href="https://www.appui
787o.ch/en/faq">APPUiO FAQ</a>. Your data stays in the zone you chose.</p> 788<p><strong>A dedicated cluster, or your own hardware.</strong> Here the answer is wherever your contract with the provider reaches, which includes further Swiss sites from both providers, or your own datacenter. Tell us the location you need and we will tell you whether we can operate there.</p> 789<p>Either way, data does not leave the country as part of normal operations.</p> 790<h3>2. Are you an independent Swiss company?</h3> 791<p>VSHN AG is a Swiss stock corporation headquartered in Zurich, founded in 2014, with over 40 employees. There is no foreign parent company and no foreign investor. Every shareholder is a Swiss citizen, on record in the <a href="https://zh.chregister.ch/cr-portal/auszug/auszug.xhtml?uid=CHE-275.566.226">commercial register of the canton of Zurich</a> under CHE-275.566.226.</p> 792<p>This is the answer behind the CLOUD Act question. US authorities can compel a company subject to US jurisdiction, which means a US parent, a US subsidiary or US operations. VSHN has none of the three.</p> 793<h3>3. Which subprocessors do you use?</h3> 794<p>For a typical engagement, one: the infrastructure provider you selected. The complete, current list is part of the <a href="https://products.vshn.ch/legal/dpa_en.html">data processing agreement</a>, which means it is a contractual commitment rather than a marketing statement, and changes to it follow the notification process in that agreement.</p> 795<h3>4. Can you sign a DPA under Art. 9 of the Swiss FADP?</h3> 796<p>Yes. Our <a href="https://products.vshn.ch/legal/dpa_en.html">data processing agreement</a> is published rather than negotiated from scratch, governed by Swiss law with Zurich as the place of jurisdiction. <a href="https://products.vshn.ch/legal/dpa_annex1_en.html">Annex 1</a> lists the technical and organizational measures, which is usually the annex your customer's data protection officer reads first.</p> 797<h3>5. Do you offer managed databases with high availability and point-in-time recovery?</h3> 798<p>Yes. <a href="https://products.vshn.ch/appcat/postgresql_cnpg.html">Managed PostgreSQL</a> runs as a primary with a replica and automatic failover, with point-in-time recovery and encryption at rest provided by the underlying cloud platform. Backup is part of the service. The same applies to the other services in the catalog, including <a href="https://products.vshn.ch/appcat/keycloak.html">Keycloak</a> for customer identity and <a href="https://products.vshn.ch/appcat/openbao.html">OpenBao</a> for secrets.</p> 799<h3>6. Which tenant isolation model do you recommend?</h3> 800<p>For regulated data, a separate database instance per major customer, with schema isolation for the tenants underneath. Our reasoning, including what that choice costs, is on the <a href="https://managed-postgresql.ch/multi-tenancy/">multi-tenancy isolation guide</a> on our PostgreSQL site.</p> 801<h3>7. Can services reach our customers' on-premises systems?</h3> 802<p>Yes. Some of your larger customers will refuse to put their data in your cloud database and will want your application to read from their own systems instead. Outbound connectivity is arranged per customer, either over defined egress addresses or a VPN tunnel, and the right shape depends on what their network team allows. This is worth a technical call rather than a checkbox.</p> 803<h3>8. Will you grant audit rights to our customers?</h3> 804<p>This is the question that decides deals, so here is the honest answer in three parts.</p> 805<p><strong>What is already covered.</strong> Most of what an auditor asks for is satisfied by evidence that exists before anyone asks. VSHN is <a href="https://www.vshn.ch/en/about/awards-certifications/">ISO 27001 certified</a> and produces an ISAE 3402 Type II report, which is an independent auditor's opinion on whether our controls were designed properly and actually operated over a defined period. Type II is the distinction that matters: it covers a period of real operation, not a snapshot. In practice this closes most of a due-diligence questionnaire without a site visit.</p> 806<p><strong>What is negotiated.</strong> Contractual audit rights for you and for your customer, including on-site audits, are agreed in the contract. We have done this before for customers under FINMA supervision, including <a href="https://www.vshn.ch/en/solutions/solutions-for-banks-and-financial-service-providers/">cantonal banks through Finnova and Acrevis Bank</a>. The clause is written for the specific engagement.</p> 807<p><strong>What we will not do.</strong> We will not tell you a clause is pre-approved before our legal team has read your customer's wording. A provider who promises that in a first email is telling you what you want to hear.</p> 808<p style="text-align:center; margin: 2rem 0"><a href="#contact" class="btn">Book an Assessment</a></p> 809 810<h2>Why this is different when you are the vendor</h2> 811<p>Nothing on this page is a requirement of yours. Every item is inherited from a customer of yours, which changes what you need from a hosting provider.</p> 812<p>You do not primarily need a platform that runs well. You need one that produces evidence: a certificate with the right scope, an audit report covering a period rather than a day, a published DPA your customer's lawyer can read without a meeting, and a provider willing to be named in your own outsourcing documentation.</p> 813<p>That is a different purchase from buying capacity, and it is why the answers above are written to be forwarded rather than summarized.</p> 814<h2>What VSHN runs for you</h2> 815<p>One contract covering the whole stack: managed Kubernetes, <a href="https://managed-postgresql.ch/">PostgreSQL</a>, <a href="https://www.managed-keycloak.ch/">Keycloak</a>, object storage, secrets management and the platform underneath. One operations team, based in Switzerland, on call around the clock. Every component is upstream open source, so the exit path is a data export rather than a rewrite.</p> 816<p>Book a call if you want the answers above checked against your customer's actual questionnaire.</p> 817 </article> 818</section> 819 820 821 822<section class="contact" id="contact"> 823 <h2>Start with an architecture assessment</h2> 824 <p class="contact-intro">
824Tell us about your infrastructure and the services you need. We'll map out how the Application Catalog fits your environment and provide a custom proposal. 825</p> 826 827 828 829 830 831 832 833 <a href="https://vshn.cal.vs.hn/appcat" target="_blank" rel="noopener" class="btn booking-btn">Book a free call</a> 834 <p class="contact-divider">Or ask your question</p> 835 836 <form class="contact-form" method="post" action="/contact/"> 837 <input type="hidden" name="csrfmiddlewaretoken" value="qOXPsZR4elXJtC4bJMCkqt205P5j0bKYFnLX8mpmHFOdTC4ZcyXBMSjFxdLoJ7Ig"> 838 839<input type="hidden" name="gclid" value=""> 840 841<input type="hidden" name="cookie_consent" value=""> 842<input type="hidden" name="utm_source" value=""> 843<input type="hidden" name="utm_medium" value=""> 844<input type="hidden" name="utm_campaign" value=""> 845<input type="hidden" name="utm_content" value=""> 846<input type="hidden" name="utm_term" value=""> 847 848 849 <input type="hidden" name="referrer" id="id_referrer" value=""> 850 <input type="hidden" name="ref" id="id_ref" value=""> 851 852 <input type="hidden" name="lang" value="en"> 853 <div class="form-row"> 854 <label for="id_name">Your Name *</label> 855 <input type="text" id="id_name" name="name" required maxlength="100"> 856 </div> 857 <div class="form-row"> 858 <label for="id_phone">Phone Number</label> 859 <input type="tel" id="id_phone" name="phone" maxlength="30"> 860 </div> 861 <div class="form-row"> 862 <label for="id_email">Your Email *</label> 863 <input type="email" id="id_email" name="email" required> 864 </div> 865 <div class="form-row"> 866 <label for="id_company">Your Company</label> 867 <input type="text" id="id_company" name="company" maxlength="100"> 868 </div> 869 <div class="form-row"> 870 <label for="id_job_title">Your Role</label> 871 <input type="text" id="id_job_title" name="job_title" maxlength="100" placeholder="e.g. CTO, Platform Engineer"> 872 </div> 873 874 <div class="form-row"> 875 <label for="id_subject">Subject *</label> 876 <input type="text" id="id_subject" name="subject" required maxlength="200"> 877 </div> 878 <div class="form-row"> 879 <label for="id_message">Your Question *</label> 880 <textarea id="id_message" name="message" rows="5" required></textarea> 881 </div> 882 883 884 <div class="form-row cap-row" data-cap-api-endpoint="/cap/" data-cap-i18n-initial-state="Verify you are human" data-cap-i18n-verifying-label="Verifyingâ¦" data-cap-i18n-solved-label="Verified" data-cap-i18n-error-label="Verification failed"></div> 885 886 <div class="form-row form-row-checkbox"> 887 <input type="checkbox" name="subscribe" id="id_subscribe" value="1"> 888 <label for="id_subscribe">Keep me updated on VSHN Application Catalog</label> 889 <small class="subscribe-consent">You can unsubscribe anytime.</small> 890 </div> 891 892 <span id="contact-success-msg" hidden>Thank you. Weâll be in touch soon!</span> 893 <button type="submit" class="btn" data-sending="Sendingâ¦"> 894 Send your question 895 </button> 896 </form> 897 898</section> 899<style> 900@keyframes contact-spin { to { transform: rotate(360deg); } } 901.btn-sending { opacity: .75; display: inline-flex; align-items: center; gap: .5em; } 902.btn-sending::before { 903 content: ''; 904 width: 1em; height: 1em; 905 border: 2px solid currentColor; 906 border-top-color: transparent; 907 border-radius: 50%; 908 animation: contact-spin .7s linear infinite; 909 flex-shrink: 0; 910} 911.form-row-checkbox { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; } 912.form-row-checkbox input[type="checkbox"] { width: auto; accent-color: var(--color-primary); } 913.form-row-checkbox label { display: inline; opacity: 1; font-size: 0.95rem; cursor: pointer; } 914.form-row-checkbox .subscribe-consent { flex-basis: 100%; font-size: 0.8rem; opacity: 0.75; margin: 0; } 915</style>
916<script> 917(function () { 918 var form = document.querySelector('.contact-form'); 919 if (!form) return; 920 921 922 form.addEventListener('submit', function (e) { 923 e.preventDefault(); 924 /* Without a solved challenge the POST is answered 403 and the visitor 925 got a generic failure. Say what is actually missing instead, and make 926 sure the widget is on screen to be solved. */ 927 if (!window.vshnCapReady(form)) { 928 window.vshnCapMount(form); 929 showError('Please complete the verification above before sending.'); 930 return; 931 } 932 var btn = form.querySelector('[type=submit]'); 933 var originalLabel = btn.textContent; 934 btn.disabled = true; 935 btn.classList.add('btn-sending'); 936 btn.textContent = btn.dataset.sending; 937 function resetBtn() { 938 btn.disabled = false; 939 btn.classList.remove('btn-sending'); 940 btn.textContent = originalLabel; 941 } 942 // Fill gclid + every UTM param from URL or localStorage. Defined in base.html 943 // so the contact and subscribe forms cannot capture different sets (#556). 944 if (typeof window.vshnFillAttribution === 'function') { 945 window.vshnFillAttribution(form); 946 } 947 // Read here because the success handler below branches on it. It used to 948 // be read only inside the booking-confirmation script, a different scope, 949 // so `source: gclid ? ...` threw a ReferenceError on every successful 950 // submit: the visitor saw the generic failure and the Contact Form goal 951 // never fired, while the lead had already been created (#624). 952 // Through vshnAttr: this page's values, or what a consented visit stored (#852). 953 var gclid = window.vshnAttr('gclid'); 954 var ref = window.vshnAttr('referrer'); 955 if (ref) { document.getElementById('id_referrer').value = ref; } 956 var refVal = window.vshnAttr('ref'); 957 if (refVal) { document.getElementById('id_ref').value = refVal; } 958 fetch(form.action, { 959 method: 'POST', 960 headers: {'X-Requested-With': 'XMLHttpRequest'}, 961 body: new FormData(form), 962 }).then(function (resp) { 963 if (resp.ok) { 964 965 loadGtag(); 966 if (typeof gtag === 'function') { 967 var email = form.querySelector('[name=email]') ? form.querySelector('[name=email]').value : ''; 968 if (email) { gtag('set', 'user_data', {'email': email}); } 969 gtag('event', 'conversion', { 970 'send_to': 'AW-18008867378/q0Q0COL-77gcELKEpotD', 971 'value': 1.0, 972 'currency': 'CHF' 973 }); 974 } 975 976 if (typeof plausible === 'function') { plausible('Contact Form', {props: {site: 'appcat_ch', source: gclid ? 'google-ads' : 'organic'}}); } 977 var err = document.querySelector('.contact-error'); if (err) err.remove(); 978 form.outerHTML = '<p class="contact-success">' + document.getElementById('contact-success-msg').textContent + '</p>'; 979 } else { 980 showError(); 981 resetBtn(); 982 } 983 }).catch(function () { 984 showError(); 985 resetBtn(); 986 }); 987 }); 988 function showError(message) { 989 var text = message || 'Something went wrong. Please check your details and try again.'; 990 var existing = document.querySelector('.contact-error'); 991 if (existing) { existing.textContent = text; return; } 992 var p = document.createElement('p'); 993 p.className = 'contact-error'; 994 p.textContent = text; 995 form.insertAdjacentElement('beforebegin', p); 996 } 997})(); 998</script>
998 999 1000 1001 1002 </main> 1003 1004 1005 1006<footer> 1007 1008 <div class="footer-body"> 1009 1010 <div class="footer-about"> 1011 <h3>About us</h3> 1012 <img src="/static/base_theme/vshn_logo_white.4ce72e59d0b1.svg" alt="VSHN Application Catalog" class="footer-logo" width="160" height="32"> 1013 1014 <p>VSHN â The DevOps Company is based in Zürich, Switzerland, and specializes in managed open-source infrastructure, cloud operations, and application platform engineering. 1015</p> 1016 <p>The VSHN Application Catalog provides fully managed open-source services (databases, identity management, secrets, and platform tooling) operated 24/7 on your private cloud or on Swiss public cloud infrastructure. 1017</p> 1018 </div> 1019 1020 1021 <div class="footer-connect"> 1022 <h3>Connect with us</h3> 1023 <ul class="footer-contact-list"> 1024 <li><a href="mailto:[email protected]">[email protected]</a></li> 1025 <li><a href="tel:+41 44 545 53 00">+41 44 545 53 00</a></li> 1026 <li>Neugasse 10, 8005 Zürich, Switzerland</li> 1027 </ul> 1028 1029 1030 1031 1032 1033 1034 1035 <ul class="footer-social"> 1036 1037 <li><a href="https://mastodon.vshn.social/@vshn" aria-label="Mastodon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" fill="currentColor" aria-hidden="true"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/></svg></a></li> 1038 1039 <li><a href="https://bsky.app/profile/vshnch.bsky.social" aria-label="Bluesky"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" fill="currentColor" aria-hidden="true"><path d="M5.202 2.857C7.954 4.922 10.913 9.11 12 11.358c1.087-2.247 4.046-6.436 6.798-8.501C20.783 1.366 24 .213 24 3.883c0 .732-.42 6.156-.667 7.037-.856 3.061-3.978 3.842-6.755 3.37 4.854.826 6.089 3.562 3.422 6.299-5.065 5.196-7.28-1.304-7.847-2.97-.104-.305-.152-.448-.153-.327 0-.121-.05.022-.153.327-.568 1.666-2.782 8.166-7.847 2.97-2.667-2.737-1.432-5.473 3.422-6.3-2.777.473-5.899-.308-6.755-3.369C.42 10.04 0 4.615 0 3.883c0-3.67 3.217-2.517 5.202-1.026"/></svg></a></li> 1040 1041 <li><a href="https://www.linkedin.com/company/vshn-ag" aria-label="LinkedIn"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" fill="currentColor" aria-hidden="true"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg></a></li> 1042 1043 <li><a href="https://github.com/vshn" aria-label="GitHub"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" fill="currentColor" aria-hidden="true"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg></a></li> 1044 1045 <li><a href="https://gitlab.com/vshn" aria-label="GitLab"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" fill="currentColor" aria-hidden="true">
1045<path d="m23.6004 9.5927-.0337-.0862L20.3.9814a.851.851 0 0 0-.3362-.405.8748.8748 0 0 0-.9997.0539.8748.8748 0 0 0-.29.4399l-2.2055 6.748H7.5375l-2.2057-6.748a.8573.8573 0 0 0-.29-.4412.8748.8748 0 0 0-.9997-.0537.8585.8585 0 0 0-.3362.4049L.4332 9.5015l-.0325.0862a6.0657 6.0657 0 0 0 2.0119 7.0105l.0113.0087.03.0213 4.976 3.7264 2.462 1.8633 1.4995 1.1321a1.0085 1.0085 0 0 0 1.2197 0l1.4995-1.1321 2.4619-1.8633 5.006-3.7489.0125-.01a6.0682 6.0682 0 0 0 2.0094-7.003z"/></svg></a></li> 1046 1047 <li><a href="https://www.facebook.com/vshn.ch/" aria-label="Facebook"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" fill="currentColor" aria-hidden="true"><path d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z"/></svg></a></li> 1048 1049 <li><a href="https://www.instagram.com/vshn_ch/" aria-label="Instagram"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" fill="currentColor" aria-hidden="true"><path d="M7.0301.084c-1.2768.0602-2.1487.264-2.911.5634-.7888.3075-1.4575.72-2.1228 1.3877-.6652.6677-1.075 1.3368-1.3802 2.127-.2954.7638-.4956 1.6365-.552 2.914-.0564 1.2775-.0689 1.6882-.0626 4.947.0062 3.2586.0206 3.6671.0825 4.9473.061 1.2765.264 2.1482.5635 2.9107.308.7889.72 1.4573 1.388 2.1228.6679.6655 1.3365 1.0743 2.1285 1.38.7632.295 1.6361.4961 2.9134.552 1.2773.056 1.6884.069 4.9462.0627 3.2578-.0062 3.668-.0207 4.9478-.0814 1.28-.0607 2.147-.2652 2.9098-.5633.7889-.3086 1.4578-.72 2.1228-1.3881.665-.6682 1.0745-1.3378 1.3795-2.1284.2957-.7632.4966-1.636.552-2.9124.056-1.2809.0692-1.6898.063-4.948-.0063-3.2583-.021-3.6668-.0817-4.9465-.0607-1.2797-.264-2.1487-.5633-2.9117-.3084-.7889-.72-1.4568-1.3876-2.1228C21.2982 1.33 20.628.9208 19.8378.6165 19.074.321 18.2017.1197 16.9244.0645 15.6471.0093 15.236-.005 11.977.0014 8.718.0076 8.31.0215 7.0301.0839m.1402 21.6932c-1.17-.0509-1.8053-.2453-2.2287-.408-.5606-.216-.96-.4771-1.3819-.895-.422-.4178-.6811-.8186-.9-1.378-.1644-.4234-.3624-1.058-.4171-2.228-.0595-1.2645-.072-1.6442-.079-4.848-.007-3.2037.0053-3.583.0607-4.848.05-1.169.2456-1.805.408-2.2282.216-.5613.4762-.96.895-1.3816.4188-.4217.8184-.6814 1.3783-.9003.423-.1651 1.0575-.3614 2.227-.4171 1.2655-.06 1.6447-.072 4.848-.079 3.2033-.007 3.5835.005 4.8495.0608 1.169.0508 1.8053.2445 2.228.408.5608.216.96.4754 1.3816.895.4217.4194.6816.8176.9005 1.3787.1653.4217.3617 1.056.4169 2.2263.0602 1.2655.0739 1.645.0796 4.848.0058 3.203-.0055 3.5834-.061 4.848-.051 1.17-.245 1.8055-.408 2.2294-.216.5604-.4763.96-.8954 1.3814-.419.4215-.8181.6811-1.3783.9-.4224.1649-1.0577.3617-2.2262.4174-1.2656.0595-1.6448.072-4.8493.079-3.2045.007-3.5825-.006-4.848-.0608M16.953 5.5864A1.44 1.44 0 1 0 18.39 4.144a1.44 1.44 0 0 0-1.437 1.4424M5.8385 12.012c.0067 3.4032 2.7706 6.1557 6.173 6.1493 3.4026-.0065 6.157-2.7701 6.1506-6.1733-.0065-3.4032-2.771-6.1565-6.174-6.1498-3.403.0067-6.156 2.771-6.1496 6.1738M8 12.0077a4 4 0 1 1 4.008 3.9921A3.9996 3.9996 0 0 1 8 12.0077"/></svg></a></li> 1050 1051 <li><a href="https://vshn.tv/" aria-label="YouTube"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" fill="currentColor" aria-hidden="true"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg></a></li> 1052 1053 </ul> 1054 1055 </div> 1056 1057 </div> 1058 1059 <div class="footer-disclaimer"> 1060 Product names and logos are trademarks of their respective owners. VSHN is not affiliated with or endorsed by these projects unless explicitly stated. 1061 1062 </div> 1063 1064 1065 <div class="footer-disclaimer footer-reviewed">
1066 Content last reviewed against its sources on 2026-09-22 1067 </div> 1068 1069 1070 <div class="footer-products"> 1071 <p class="footer-products-heading">Other VSHN Products and Services</p> 1072 <ul class="footer-products-list"> 1073 1074 <li> 1075 1076 1077 1078 1079 <a href="https://www.vshn.ch/" title="VSHN â The DevOps Company"> 1080 <img src="/static/base_theme/vshn_favicon.94abf305c7da.svg" alt="VSHN â The DevOps Company" width="32" height="32" loading="lazy"> 1081 </a> 1082 1083 </li> 1084 1085 <li> 1086 1087 1088 1089 1090 <a href="https://www.appuio.ch" title="APPUiO Cloud"> 1091 <img src="/static/base_theme/appuio_favicon.f4a514861d09.svg" alt="APPUiO Cloud" width="32" height="32" loading="lazy"> 1092 </a> 1093 1094 </li> 1095 1096 <li> 1097 1098 1099 1100 1101 <a href="https://www.servala.com" title="Servala"> 1102 <img src="/static/base_theme/servala_favicon.6d74861890d7.svg" alt="Servala" width="32" height="32" loading="lazy"> 1103 </a> 1104 1105 </li> 1106 1107 <li> 1108 1109 1110 1111 1112 <a href="https://k8up.io" title="K8up â Kubernetes backup operator"> 1113 <img src="/static/base_theme/k8up_favicon.c1c4eccc6d93.svg" alt="K8up â Kubernetes backup operator" width="32" height="32" loading="lazy"> 1114 </a> 1115 1116 </li> 1117 1118 <li> 1119 1120 1121 1122 1123 <a href="https://codey.ch" title="Codey â Managed Forgejo Platform"> 1124 <img src="/static/base_theme/codey_favicon.57949baf17c7.svg" alt="Codey â Managed Forgejo Platform" width="32" height="32" loading="lazy"> 1125 </a> 1126 1127 </li> 1128 1129 <li> 1130 1131 1132 1133 1134 <a href="https://syn.tools" title="Project Syn â Kubernetes configuration management"> 1135 <img src="/static/base_theme/syn_favicon.8f903478e272.svg" alt="Project Syn â Kubernetes configuration management" width="32" height="32" loading="lazy"> 1136 </a> 1137 1138 </li> 1139 1140 <li> 1141 1142 1143 1144 1145 <a href="https://espejote.io" title="Espejote â the in-cluster Kubernetes templating controller"> 1146 <img src="/static/base_theme/espejote_favicon.a6f12fd3b51c.svg" alt="Espejote â the in-cluster Kubernetes templating controller" width="32" height="32" loading="lazy"> 1147 </a> 1148 1149 </li> 1150 1151 <li> 1152 1153 1154 1155 1156 <a href="https://www.gitlab.ch" title="GitLab Competence Center Switzerland"> 1157 <img src="/static/gitlab_ch/favicon.cd43552255e2.svg" alt="GitLab Competence Center Switzerland" width="32" height="32" loading="lazy"> 1158 </a> 1159 1160 </li> 1161 1162 <li> 1163 1164 1165 1166 1167 <a href="https://www.forgejo.ch" title="Forgejo Competence Center Switzerland"> 1168 <img src="/static/forgejo_ch/favicon.4d282f56f630.svg" alt="Forgejo Competence Center Switzerland" width="32" height="32" loading="lazy"> 1169 </a> 1170 1171 </li> 1172 1173 <li> 1174 1175 1176 1177 1178 <a href="https://www.managed-keycloak.ch" title="Managed Keycloak Switzerland"> 1179 <img src="/static/managed_keycloak_ch/favicon.a59177cd8ae4.svg" alt="Managed Keycloak Switzerland" width="32" height="32" loading="lazy"> 1180 </a> 1181 1182 </li> 1183 1184 <li> 1185 1186 1187 1188 1189 <a href="https://www.managed-redis.ch" title="Managed Redis Switzerland"> 1190 <img src="/static/managed_redis_ch/favicon.2ae9319b61c7.svg" alt="Managed Redis Switzerland" width="32" height="32" loading="lazy"> 1191 </a> 1192 1193 </li> 1194 1195 <li> 1196 1197 1198 1199 1200 <a href="https://www.managed-rabbitmq.ch" title="Managed RabbitMQ Switzerland"> 1201 <img src="/static/managed_rabbitmq_ch/favicon.45443a1eb39e.svg" alt="Managed RabbitMQ Switzerland" width="32" height="32" loading="lazy"> 1202 </a> 1203 1204 </li> 1205 1206 <li> 1207 1208 1209 1210 1211 <a href="https://www.enterprisedb.ch" title="EDB PostgreSQL Competence Center Switzerland"> 1212 <img src="/static/enterprisedb_ch/favicon.71483eb10e0a.svg" alt="EDB PostgreSQL Competence Center Switzerland" width="32" height="32" loading="lazy"> 1213 </a> 1214 1215 </li> 1216 1217 <li> 1218 1219 1220 1221 1222 <a href="https://www.managed-postgresql.ch" title="Managed PostgreSQL Switzerland">
1223 <img src="/static/managed_postgresql_ch/favicon.691b0ea86a36.svg" alt="Managed PostgreSQL Switzerland" width="32" height="32" loading="lazy"> 1224 </a> 1225 1226 </li> 1227 1228 <li> 1229 1230 1231 1232 1233 <a href="https://www.managed-mariadb.ch" title="Managed MariaDB Switzerland"> 1234 <img src="/static/managed_mariadb_ch/favicon.10affd1383f4.svg" alt="Managed MariaDB Switzerland" width="32" height="32" loading="lazy"> 1235 </a> 1236 1237 </li> 1238 1239 <li> 1240 1241 1242 1243 1244 <a href="https://www.the-devops-company.ch" title="The DevOps Company Switzerland"> 1245 <img src="/static/the_devops_company_ch/favicon.bf4f77e1e291.svg" alt="The DevOps Company Switzerland" width="32" height="32" loading="lazy"> 1246 </a> 1247 1248 </li> 1249 1250 <li> 1251 1252 1253 1254 1255 <a href="https://www.cloudnativealliance.ch" title="Swiss Cloud Native Alliance"> 1256 <img src="/static/cloudnativealliance_ch/favicon.5773b952fcee.svg" alt="Swiss Cloud Native Alliance" width="32" height="32" loading="lazy"> 1257 </a> 1258 1259 </li> 1260 1261 <li> 1262 1263 1264 1265 1266 <a href="https://www.cloud-native.ch" title="Cloud-Native Competence Center Switzerland"> 1267 <img src="/static/cloud_native_ch/favicon.1ec18989cfce.svg" alt="Cloud-Native Competence Center Switzerland" width="32" height="32" loading="lazy"> 1268 </a> 1269 1270 </li> 1271 1272 <li> 1273 1274 1275 1276 1277 <a href="https://www.llmops.ch" title="LLMOps Operations Competence Center Switzerland"> 1278 <img src="/static/llmops_ch/favicon.ad16f42d80af.svg" alt="LLMOps Operations Competence Center Switzerland" width="32" height="32" loading="lazy"> 1279 </a> 1280 1281 </li> 1282 1283 <li> 1284 1285 1286 1287 1288 <a href="https://www.varnish.ch" title="Varnish Competence Center Switzerland"> 1289 <img src="/static/varnish_ch/favicon.a282f0cddaa1.svg" alt="Varnish Competence Center Switzerland" width="32" height="32" loading="lazy"> 1290 </a> 1291 1292 </li> 1293 1294 <li> 1295 1296 1297 1298 1299 <a href="https://www.cnc-meetup.ch" title="Cloud Native Computing Meetup Switzerland"> 1300 <img src="/static/cnc_meetup_ch/favicon.e218be080519.svg" alt="Cloud Native Computing Meetup Switzerland" width="32" height="32" loading="lazy"> 1301 </a> 1302 1303 </li> 1304 1305 <li> 1306 1307 1308 1309 1310 <a href="https://www.openbao.ch" title="OpenBao Competence Center Switzerland"> 1311 <img src="/static/openbao_ch/favicon.861bf8e9098b.svg" alt="OpenBao Competence Center Switzerland" width="32" height="32" loading="lazy"> 1312 </a> 1313 1314 </li> 1315 1316 <li> 1317 1318 1319 1320 1321 <a href="https://www.nginx-hosting.net" title="Managed Nginx Hosting"> 1322 <img src="/static/nginx_hosting_net/favicon.ca83f70b683d.svg" alt="Managed Nginx Hosting" width="32" height="32" loading="lazy"> 1323 </a> 1324 1325 </li> 1326 1327 <li> 1328 1329 1330 1331 1332 <a href="https://www.swiss-cdn.ch" title="Swiss CDN Competence Center"> 1333 <img src="/static/swiss_cdn_ch/favicon.4a8beb7677f8.svg" alt="Swiss CDN Competence Center" width="32" height="32" loading="lazy"> 1334 </a> 1335 1336 </li> 1337 1338 <li> 1339 1340 1341 1342 1343 <a href="https://www.vllm.ch" title="vLLM Operations Competence Center Switzerland"> 1344 <img src="/static/vllm_ch/favicon.defc1abe016d.svg" alt="vLLM Operations Competence Center Switzerland" width="32" height="32" loading="lazy"> 1345 </a> 1346 1347 </li> 1348 1349 <li> 1350 1351 1352 1353 1354 <a href="https://www.litellm.ch" title="LiteLLM Operations Competence Center Switzerland"> 1355 <img src="/static/litellm_ch/favicon.50b7635a7b35.svg" alt="LiteLLM Operations Competence Center Switzerland" width="32" height="32" loading="lazy"> 1356 </a> 1357 1358 </li> 1359 1360 <li> 1361 1362 1363 1364 1365 <a href="https://www.managed-openshift.ch" title="OpenShift Competence Center Switzerland">
1366 <img src="/static/managed_openshift_ch/favicon.65359427e20c.svg" alt="OpenShift Competence Center Switzerland" width="32" height="32" loading="lazy"> 1367 </a> 1368 1369 </li> 1370 1371 <li> 1372 1373 1374 1375 1376 <a href="https://www.kubernetes-experten.ch" title="Kubernetes Competence Center Switzerland"> 1377 <img src="/static/kubernetes_experten_ch/favicon.3ced4efd927e.svg" alt="Kubernetes Competence Center Switzerland" width="32" height="32" loading="lazy"> 1378 </a> 1379 1380 </li> 1381 1382 <li> 1383 1384 1385 1386 1387 <a href="https://www.llm-d.ch" title="llm-d Operations Competence Center Switzerland"> 1388 <img src="/static/llm_d_ch/favicon.55c2f46df3bf.svg" alt="llm-d Operations Competence Center Switzerland" width="32" height="32" loading="lazy"> 1389 </a> 1390 1391 </li> 1392 1393 <li> 1394 1395 1396 1397 1398 <span class="product-active" title="VSHN Application Catalog" aria-current="page"> 1399 <img src="/static/appcat_ch/favicon.326f623176ef.svg" alt="VSHN Application Catalog" width="32" height="32" loading="lazy"> 1400 </span> 1401 1402 </li> 1403 1404 <li> 1405 1406 1407 1408 1409 <a href="https://www.managed-kubernetes.ch" title="Managed Kubernetes Switzerland"> 1410 <img src="/static/managed_kubernetes_ch/favicon.97431a66849d.svg" alt="Managed Kubernetes Switzerland" width="32" height="32" loading="lazy"> 1411 </a> 1412 1413 </li> 1414 1415 <li> 1416 1417 1418 1419 1420 <a href="https://ocre.vshn.ch" title="OCRE Cloud for Research & Education"> 1421 <img src="/static/ocre_vshn_ch/favicon.dc29c3e0ebd9.svg" alt="OCRE Cloud for Research & Education" width="32" height="32" loading="lazy"> 1422 </a> 1423 1424 </li> 1425 1426 <li> 1427 1428 1429 1430 1431 <a href="https://www.application-operations.ch" title="Application Operations Switzerland"> 1432 <img src="/static/application_operations_ch/favicon.0183e9cf1b04.svg" alt="Application Operations Switzerland" width="32" height="32" loading="lazy"> 1433 </a> 1434 1435 </li> 1436 1437 <li> 1438 1439 1440 1441 1442 <a href="https://www.developer-platform.ch" title="Developer Platform Switzerland"> 1443 <img src="/static/developer_platform_ch/favicon.2ad5f638b21c.svg" alt="Developer Platform Switzerland" width="32" height="32" loading="lazy"> 1444 </a> 1445 1446 </li> 1447 1448 </ul> 1449 </div> 1450 1451 <div class="footer-copy"> 1452 Copyright © VSHN AG 1453 </div> 1454 1455</footer> 1456 1457 1458 1459 <div id="CookieConsent" style="display:none" role="dialog" aria-label="Cookie consent"> 1460 <div class="modal-content"> 1461 <div class="modal-header"> 1462 <p>We use cookies for analytics and advertising. 1463 <a href="https://www.vshn.ch/en/privacy-policy/" target="_blank" rel="noopener">Privacy policy</a></p> 1464 </div> 1465 <div> 1466 <button id="CookieConsentDeclined">Reject</button> 1467 <button id="CookieConsentAccepted">Accept</button> 1468 </div> 1469 </div> 1470 </div> 1471
1471<script> 1472 (function() { 1473 // LinkedIn Insight Tag â loaded only on cookie consent 1474 function loadLinkedInInsight() { 1475 if (window._linkedin_loaded) return; 1476 window._linkedin_loaded = true; 1477 1478 window._linkedin_partner_id = '6349306'; 1479 window._linkedin_data_partner_ids = window._linkedin_data_partner_ids || []; 1480 window._linkedin_data_partner_ids.push(window._linkedin_partner_id); 1481 var s = document.getElementsByTagName('script')[0]; 1482 var b = document.createElement('script'); 1483 b.type = 'text/javascript'; b.async = true; 1484 b.src = 'https://snap.licdn.com/li.lms-analytics/insight.min.js'; 1485 s.parentNode.insertBefore(b, s); 1486 1487 } 1488 1489 var choice = localStorage.getItem('cookie_consent'); 1490 if (choice === 'granted') { 1491 loadGtag(); 1492 gtag('set', 'ads_data_redaction', false); 1493 gtag('consent', 'update', { 1494 'analytics_storage': 'granted', 1495 'ad_storage': 'granted', 1496 'ad_user_data': 'granted', 1497 'ad_personalization': 'granted' 1498 }); 1499 loadLinkedInInsight(); 1500 if (typeof plausible === 'function') { plausible('Cookie Consent', {props: {action: 'accepted (returning)'}}); } 1501 } else if (choice === 'denied') { 1502 if (typeof plausible === 'function') { plausible('Cookie Consent', {props: {action: 'declined (returning)'}}); } 1503 } else { 1504 document.getElementById('CookieConsent').style.display = 'flex'; 1505 } 1506 document.getElementById('CookieConsentAccepted').addEventListener('click', function() { 1507 localStorage.setItem('cookie_consent', 'granted'); 1508 // Attribution waits for this click (#852): write what this page captured. 1509 if (window.vshnPersistAttribution) window.vshnPersistAttribution(); 1510 loadGtag(); 1511 gtag('set', 'ads_data_redaction', false); 1512 gtag('consent', 'update', { 1513 'analytics_storage': 'granted', 1514 'ad_storage': 'granted', 1515 'ad_user_data': 'granted', 1516 'ad_personalization': 'granted' 1517 }); 1518 loadLinkedInInsight(); 1519 if (typeof plausible === 'function') { plausible('Cookie Consent', {props: {action: 'accepted'}}); } 1520 document.getElementById('CookieConsent').style.display = 'none'; 1521 }); 1522 document.getElementById('CookieConsentDeclined').addEventListener('click', function() { 1523 localStorage.setItem('cookie_consent', 'denied'); 1524 if (typeof plausible === 'function') { plausible('Cookie Consent', {props: {action: 'declined'}}); } 1525 document.getElementById('CookieConsent').style.display = 'none'; 1526 }); 1527 })(); 1528 </script>
1528 1529 1530 1531 1532 1533
1533<script> 1534 (function() { 1535 if (typeof plausible !== 'function') return; 1536 1537 // 1. Scroll depth milestones (fire once per threshold) 1538 var scrollFired = {}; 1539 var docHeight = 0; 1540 function onScroll() { 1541 if (!docHeight) docHeight = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight); 1542 var pct = Math.round(100 * (window.scrollY + window.innerHeight) / docHeight); 1543 [25, 50, 75, 100].forEach(function(t) { 1544 if (pct >= t && !scrollFired[t]) { 1545 scrollFired[t] = true; 1546 plausible('Scroll Depth', {props: {depth: t + '%'}}); 1547 } 1548 }); 1549 } 1550 window.addEventListener('scroll', onScroll, {passive: true}); 1551 // Check immediately in case page is short 1552 setTimeout(onScroll, 1000); 1553 1554 // 2. Section visibility (fire once per section) 1555 var sectionFired = {}; 1556 if ('IntersectionObserver' in window) { 1557 var onSection = function(entries) { 1558 entries.forEach(function(e) { 1559 if (e.isIntersecting && !sectionFired[e.target.id]) { 1560 sectionFired[e.target.id] = true; 1561 plausible('Section Viewed', {props: {section: e.target.id}}); 1562 } 1563 }); 1564 }; 1565 // threshold is a fraction of the *target*, not of the viewport. A section 1566 // taller than innerHeight / 0.15 can never reach a ratio of 0.15 however 1567 // far you scroll, so a fixed threshold silently never fires on a long 1568 // one. That matters here: wrapping a markdown article in one section[id] 1569 // makes exactly such a target, so the wrapper alone would have fixed 1570 // nothing on the longest pages and would have been hard to notice. 1571 // Scale it per section: fire once half a viewport of a tall one is 1572 // showing, and keep 0.15 exactly for short ones so index pages, which 1573 // were never broken, behave as before. 1574 document.querySelectorAll('section[id]').forEach(function(s) { 1575 var t = Math.min(0.15, (window.innerHeight * 0.5) / Math.max(s.offsetHeight, 1)); 1576 new IntersectionObserver(onSection, {threshold: t}).observe(s); 1577 }); 1578 } 1579 1580 // 3. Booking button click 1581 var bookingBtn = document.querySelector('.booking-btn'); 1582 if (bookingBtn) { 1583 bookingBtn.addEventListener('click', function() { 1584 plausible('Booking Click'); 1585 }); 1586 } 1587 1588 // 4. Form interaction start (first field focus) 1589 var formStarted = false; 1590 var form = document.querySelector('.contact-form'); 1591 if (form) { 1592 form.addEventListener('focusin', function() { 1593 if (!formStarted) { 1594 formStarted = true; 1595 plausible('Form Started'); 1596 } 1597 }); 1598 } 1599 1600 // 5. FAQ expansion clicks 1601 document.querySelectorAll('.faq details summary').forEach(function(s) { 1602 s.addEventListener('click', function() { 1603 var q = s.textContent.trim().substring(0, 80); 1604 plausible('FAQ Opened', {props: {question: q}}); 1605 }); 1606 }); 1607 1608 // 5b. Language switcher clicks. 1609 // The one direct observation of a reader who landed in the wrong language 1610 // and went looking for the other one, which is the population #618's 1611 // banner would serve (#647). `to` carries the language being switched to, 1612 // so direction is read rather than inferred. 1613 // 1614 // The click navigates away, so a bare plausible() call races the 1615 // navigation and is lost often enough to make the number untrustworthy. 1616 // Fire with the callback and navigate from it, with a timeout so 1617 // analytics can never block or delay the page the reader asked for. 1618 document.querySelectorAll('.lang-switcher a').forEach(function(a) { 1619 a.addEventListener('click', function(e) { 1620 // Modified clicks open a new tab; the page is not going anywhere, so 1621 // fire and get out of the way. 1622 if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) { 1623 plausible('Language Switch', {props: {to: a.textContent.trim()}}); 1624 return; 1625 } 1626 if (a.classList.contains('active')) return; 1627 e.preventDefault(); 1628 var href = a.href; 1629 var done = false;
1630 function go() { 1631 if (done) return; 1632 done = true; 1633 window.location = href; 1634 } 1635 setTimeout(go, 500); 1636 plausible('Language Switch', { 1637 props: {to: a.textContent.trim()}, 1638 callback: go 1639 }); 1640 }); 1641 }); 1642 1643 // 6. CTA button clicks. 1644 // nav-cta and pricing-cta are structural classes the button needs for its 1645 // layout, so a per-site template cannot drop the hook while keeping the 1646 // button. btn-primary carries no styling and is a marker, which is exactly 1647 // how codey.ch ended up with no tracked call to action at all. 1648 document.querySelectorAll('.btn-primary, .btn-secondary, .nav-cta, .pricing-cta').forEach(function(btn) { 1649 btn.addEventListener('click', function() { 1650 plausible('CTA Click', {props: { 1651 label: btn.textContent.trim().substring(0, 60), 1652 // The nav CTA sits outside every <section> and appears on every page. 1653 // Without a name of its own it would land in `unknown`, mixed with 1654 // anything else that ever escapes a section. 1655 section: (btn.closest('section') || {}).id || (btn.closest('nav') ? 'nav' : 'unknown') 1656 }}); 1657 }); 1658 }); 1659 1660 // 7. Servala provisioning links (the self-service order handoff). 1661 // Bound by href, not by class: the provisioning buttons are rendered by 1662 // per-site templates, and a class hook is what a fork silently drops. 1663 document.querySelectorAll('a[href*="portal.servala.com"]').forEach(function(link) { 1664 link.addEventListener('click', function() { 1665 plausible('Servala Portal Click', {props: { 1666 label: link.textContent.trim().substring(0, 60), 1667 section: (link.closest('section') || {}).id || 'unknown' 1668 }}); 1669 }); 1670 }); 1671 })(); 1672 </script>
1672 1673 1674 1675
1675<script> 1676 (function() { 1677 var section = document.getElementById('testimonial'); 1678 if (!section) return; 1679 var slider = section.querySelector('.testimonial-slider'); 1680 if (!slider) return; 1681 var items = section.querySelectorAll('.testimonial-item'); 1682 var dots = section.querySelectorAll('.testimonial-dot'); 1683 var arrows = section.querySelectorAll('.testimonial-arrow'); 1684 if (items.length < 2) return; 1685 1686 var current = Math.round(slider.scrollLeft / slider.offsetWidth); 1687 var autoTimer; 1688 1689 function goTo(i) { 1690 current = ((i % items.length) + items.length) % items.length; 1691 slider.scrollTo({ left: current * slider.offsetWidth, behavior: 'smooth' }); 1692 dots.forEach(function(d, j) { 1693 d.classList.toggle('active', j === current); 1694 }); 1695 } 1696 1697 function startAuto() { 1698 stopAuto(); 1699 autoTimer = setInterval(function() { goTo(current + 1); }, 6000); 1700 } 1701 function stopAuto() { clearInterval(autoTimer); } 1702 1703 var scrollTimer; 1704 slider.addEventListener('scroll', function() { 1705 clearTimeout(scrollTimer); 1706 scrollTimer = setTimeout(function() { 1707 current = Math.round(slider.scrollLeft / slider.offsetWidth); 1708 dots.forEach(function(d, j) { 1709 d.classList.toggle('active', j === current); 1710 }); 1711 }, 100); 1712 }); 1713 dots.forEach(function(d) { 1714 d.addEventListener('click', function() { stopAuto(); goTo(parseInt(d.dataset.index)); startAuto(); }); 1715 }); 1716 arrows.forEach(function(a) { 1717 a.addEventListener('click', function() { stopAuto(); goTo(current + parseInt(a.dataset.dir)); startAuto(); }); 1718 }); 1719 1720 // Pause auto-advance when not visible 1721 var observer = new IntersectionObserver(function(entries) { 1722 entries[0].isIntersecting ? startAuto() : stopAuto(); 1723 }, { threshold: 0.3 }); 1724 observer.observe(section); 1725 })(); 1726 </script>
1726 1727</body> 1728</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.