PageSourceSearch

https://www.appcat.ch/compliance/

html appcat.ch collected 2026-09-25 22:12:56 UTC 74,902 bytes, 1,728 lines download raw bytes

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">&#9776;</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&#x27;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 &amp; Education">
1421          <img src="/static/ocre_vshn_ch/favicon.dc29c3e0ebd9.svg" alt="OCRE Cloud for Research &amp; 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.