Configuring HTTPS for Local Development

This guide is part of Web Share API & Security Contexts.

The Web Share API refuses to run outside a secure context. For simple localhost testing that is already satisfied — browsers treat http://localhost and http://127.0.0.1 as secure by default. But the moment you need to test on a real Android or iOS device, connect via a LAN IP, or use a custom .test domain, you hit a wall: the browser reports window.isSecureContext === false, navigator.share is undefined, and nothing works.

This guide covers the full workflow: validating whether you actually need TLS at all, generating trusted certificates with mkcert, binding them to Vite or Webpack Dev Server, and verifying the result on physical devices. It ends with a complete share invocation function and graceful fallback for environments where the API is unavailable.

Problem Framing

Without HTTPS, three common local development scenarios silently break:

  • LAN device testing — loading http://192.168.x.x:3000 on a physical phone reports an insecure context even though the content is local
  • Custom hostnames.test or .local domains do not inherit the localhost exemption; they need a trusted certificate
  • Android WebView and some enterprise Chromium builds — these do not honour the localhost secure-context exemption and require explicit TLS even on loopback

The result in each case is identical: navigator.share is either undefined or throws NotAllowedError, and DevTools shows This page is not secure in the Security panel.

Prerequisites

Before proceeding, confirm the following:

Browser Support Snapshot

Browser Minimum version Secure-context enforcement Notes
Chrome (Android) 61 Enforced LAN IPs require valid TLS; localhost exempt
Safari (iOS) 15.1 Enforced Requires valid TLS for all non-localhost origins
Firefox (desktop) 71 Enforced Needs nss for mkcert CA acceptance
Edge (Chromium) 79 Enforced Identical to Chrome behaviour
Samsung Internet 8.2 Enforced LAN IPs require valid TLS
Android WebView Any Strict Does not honour localhost exemption

Step-by-Step Implementation

Step 1 — Validate Your Current Environment

Run this in your browser’s DevTools Console to check whether you need TLS at all:

export function validateSecureEnvironment() {
  const diagnostics = {
    isSecure: window.isSecureContext,
    protocol: location.protocol,
    hostname: location.hostname,
    hasShare: typeof navigator.share === 'function',
    hasCanShare: typeof navigator.canShare === 'function',
    origin: location.origin
  };

  if (!diagnostics.isSecure) {
    console.warn(
      '[Env] Insecure context — navigator.share will be unavailable.',
      diagnostics
    );
  } else {
    console.info('[Env] Secure context confirmed.', diagnostics);
  }

  return diagnostics;
}

If isSecure is true and hasShare is true on your current http://localhost setup, you can test navigator.share without any certificate work. Proceed to the certificate steps only if you need LAN device access or a custom hostname.

Cross-reference your target browsers against the browser support matrix for the Web Share API before setting up TLS — some older browser versions have additional quirks beyond the secure-context requirement.

Step 2 — Install mkcert and Generate Certificates

mkcert creates a local Certificate Authority (CA), installs it into your OS trust store, and issues domain-specific certificates. Browsers trust these without security warnings because the CA is in the system trust store.

# macOS
brew install mkcert
brew install nss   # required for Firefox

# Windows (run as Administrator)
choco install mkcert

# Linux (Debian/Ubuntu)
sudo apt install libnss3-tools
curl -L https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-linux-amd64 \
  -o mkcert && chmod +x mkcert && sudo mv mkcert /usr/local/bin/

Then install the local CA and generate your certificate:

# Install the local CA into OS and browser trust stores (run once per machine)
mkcert -install

# Create the certs/ directory in your project root
mkdir -p certs

# Generate a certificate covering localhost, loopback IPs, and your LAN IP.
# Replace 192.168.1.100 with your actual LAN IP from ifconfig/ip a.
mkcert -key-file certs/localhost-key.pem \
       -cert-file certs/localhost.pem \
       localhost 127.0.0.1 192.168.1.100 ::1

After running mkcert -install, restart Chrome and Firefox so they pick up the new CA. If you skip the restart, you will see NET::ERR_CERT_AUTHORITY_INVALID even though the certificate is technically valid.

Add the certs/ directory to .gitignore — certificates contain private keys and must not be committed:

echo "certs/" >> .gitignore

Step 3 — Bind Certificates to Your Dev Server

Vite (vite.config.js or vite.config.ts)

import { defineConfig } from 'vite';
import fs from 'node:fs';
import path from 'node:path';

const CERT_DIR = path.resolve('./certs');

export default defineConfig({
  server: {
    https: {
      key: fs.readFileSync(path.join(CERT_DIR, 'localhost-key.pem')),
      cert: fs.readFileSync(path.join(CERT_DIR, 'localhost.pem'))
    },
    host: '0.0.0.0',    // Expose on all interfaces for LAN device access
    port: 3000,
    strictPort: true     // Fail fast if 3000 is in use rather than silently rebinding
  }
});

Webpack Dev Server (webpack.config.js)

import fs from 'node:fs';
import path from 'node:path';

const CERT_DIR = path.resolve('./certs');

export default {
  devServer: {
    https: {
      key: fs.readFileSync(path.join(CERT_DIR, 'localhost-key.pem')),
      cert: fs.readFileSync(path.join(CERT_DIR, 'localhost.pem'))
    },
    host: '0.0.0.0',
    port: 3000
  }
};

After restarting the dev server, open https://localhost:3000 (note the https://). DevTools → Security should show “Connection is secure” with the mkcert CA name.

For a full walkthrough of testing on an iOS Simulator and Android emulator in addition to physical devices, see the step-by-step guide to testing the Web Share API on localhost.

Step 4 — Implement Share with Payload Validation and Error Boundary

With a confirmed secure context, wire up a share function that validates the payload before calling navigator.share. This uses navigator.canShare() to guard against DataError from unsupported file types or malformed payloads:

/**
 * Triggers the native share sheet with progressive enhancement.
 * Falls back to clipboard or prompt() when the API is unavailable.
 *
 * @param {{ title?: string, text?: string, url?: string }} shareData
 * @returns {Promise<void>}
 */
export async function triggerShare(shareData) {
  if (!window.isSecureContext) {
    return fallbackShare(shareData);
  }

  if (typeof navigator.share !== 'function') {
    return fallbackShare(shareData);
  }

  const payload = {
    title: shareData.title ?? document.title,
    text: shareData.text ?? '',
    url: shareData.url ?? window.location.href
  };

  // canShare() validates the payload without triggering the permission prompt
  if (typeof navigator.canShare === 'function' && !navigator.canShare(payload)) {
    console.warn('[Share] Payload rejected by canShare(). Routing to fallback.');
    return fallbackShare(shareData);
  }

  try {
    await navigator.share(payload);
  } catch (err) {
    if (err.name === 'AbortError') {
      // User dismissed the share sheet — this is intentional, not a failure
      return;
    }
    if (err.name === 'NotAllowedError') {
      // No user gesture, or the API is blocked by permissions policy
      console.error('[Share] NotAllowedError — ensure share is triggered from a click/tap handler.');
      return fallbackShare(shareData);
    }
    console.error('[Share] Unexpected error:', err.name, err.message);
    return fallbackShare(shareData);
  }
}

Payload Validation and Error Boundary

The table below maps every DOMException you can receive from navigator.share() to its cause and recovery:

DOMException Cause Recovery
AbortError User dismissed the share sheet Discard — normal user behaviour
NotAllowedError No user gesture, or permissions policy blocks sharing Ensure call is inside a click/tap handler
DataError Payload has no valid fields, or contains an unshareable file type Validate with canShare() before calling share()
InvalidStateError A share dialog is already open Debounce the share button to prevent double-tap
/**
 * Clipboard + prompt() fallback for environments where navigator.share is unavailable.
 *
 * @param {{ url?: string }} shareData
 * @returns {Promise<void>}
 */
export async function fallbackShare(shareData) {
  const targetUrl = shareData.url ?? window.location.href;

  if (navigator.clipboard?.writeText) {
    try {
      await navigator.clipboard.writeText(targetUrl);
      notifyUser('Link copied to clipboard');
    } catch {
      showCopyPrompt(targetUrl);
    }
  } else {
    showCopyPrompt(targetUrl);
  }
}

function showCopyPrompt(url) {
  // prompt() provides a built-in copy UI in all browsers with zero dependencies
  window.prompt('Copy this link:', url);
}

function notifyUser(message) {
  // Replace with your application's toast or notification component
  console.info(`[Share] ${message}`);
}

For richer fallback flows — including QR code generation and SMS URI fallback — see the permission flows section.

Platform Gotchas

iOS Safari (15.1+)

navigator.share() must be called directly inside a user gesture handler. Wrapping the call in a setTimeout — even with a 0 ms delay — breaks the gesture requirement and causes NotAllowedError. Keep the await navigator.share() call in the same synchronous call stack as the click or touchend event.

Android Chrome (61+)

LAN IP origins (192.168.x.x) are never treated as secure unless they have a valid TLS certificate. The mkcert setup in Step 2 covers this. If you see the share button disabled on the physical device but working on desktop, the almost certain cause is a missing or untrusted certificate on that LAN origin.

Android WebView

WebView does not honour the localhost secure-context exemption. If your web content is loaded inside a WebView, you need either a valid HTTPS origin or to set webView.setWebContentsDebuggingEnabled(true) and load content from https:// URLs even for local builds. Consider using the Web Share Target API registration instead of direct navigator.share invocations in hybrid apps.

Firefox (desktop)

Firefox 71+ supports navigator.share on desktop but only on Windows — the Linux and macOS Firefox builds do not expose navigator.share on desktop. Feature-detect with typeof navigator.share === 'function' before every call; never assume the API is present because another browser on the same machine supports it.

Mixed content

If your HTTPS local page loads any asset — image, script, or API call — over http://, the browser downgrades the context to “mixed content” and may block the resource or set isSecureContext to false. Keep all local dev assets on the same HTTPS origin.

Certificate Decision Flow

The diagram below shows when you need explicit TLS and which path to follow:

HTTPS Local Development Decision Flow A flowchart showing the decision points for determining whether TLS certificate setup is needed for local Web Share API testing. Starting from the question of whether the origin is localhost or 127.0.0.1, branching through LAN IP, custom domain, and WebView scenarios to either a confirmed secure context or mkcert setup. Your dev origin localhost or 127.0.0.1? Yes WebView? No Secure context ✓ No TLS needed No LAN IP or custom hostname? Yes Run mkcert Steps 2–3 No Unknown origin — diagnose

Testing and Verification

Once HTTPS is running, run through this checklist on every target platform:

Desktop verification (Chrome/Firefox/Edge)

  1. Open https://localhost:3000 — confirm the padlock icon appears, no “Not Secure” warning
  2. Open DevTools → Console, paste and run validateSecureEnvironment() from Step 1 — confirm isSecure: true and hasShare: true
  3. Open DevTools → Security — confirm the certificate is issued by “mkcert” CA, not a self-signed unknown CA

Physical Android device

  1. Confirm the device is on the same Wi-Fi network as your development machine
  2. Navigate to https://192.168.x.x:3000 (replace with your LAN IP from Step 2)
  3. If you see a certificate warning, the mkcert CA is not trusted on the device — Android does not read the OS trust store for browser connections; you need to install the CA root certificate from $(mkcert -CAROOT)/rootCA.pem via Settings → Security → Install a certificate
  4. Run validateSecureEnvironment() in Chrome for Android’s remote DevTools console

Physical iOS device

  1. Navigate to https://192.168.x.x:3000 in Safari
  2. If Safari shows an untrusted certificate, AirDrop rootCA.pem to the device and install it via Settings → General → VPN & Device Management, then enable full trust at Settings → General → About → Certificate Trust Settings
  3. Confirm isSecureContext is true before attempting share

Common Pitfalls

  • Custom .test domain without TLS: Only localhost and 127.0.0.1 have browser exemptions. Custom hostnames always require a trusted certificate regardless of whether they resolve locally.
  • Untrusted self-signed certificates: Generating a certificate without running mkcert -install first produces an unknown CA error. Always install the CA before generating certificates.
  • Android not trusting the mkcert CA: Android devices do not automatically trust user-installed CAs for app traffic. For browser testing, manually install the root CA as described above. For WebView testing, you need a network security config in the native app.
  • Port reuse across servers: Running two HTTPS dev servers on different ports both need valid certificates for their respective hostnames. The mkcert certificate covers the hostnames listed at generation time, not individual ports.
  • iOS gesture requirement: Calling navigator.share() from inside async code that was awaited before reaching the call can break the gesture link on iOS. Keep the share invocation as close to the event handler as possible.

FAQ

Does localhost automatically qualify as a secure context for the Web Share API?

Yes. Modern browsers treat localhost and 127.0.0.1 as secure contexts by default. You only need TLS when testing on a LAN IP, a custom domain, or in environments that do not honour the localhost exemption.

How do I make my local certificate trusted by browsers?

Run mkcert -install once to generate a local Certificate Authority and install it into your OS trust store. This prevents browser security warnings and ensures window.isSecureContext evaluates to true for your local domain.

What happens if the user cancels the native share dialog?

navigator.share() rejects with AbortError. Catch this specific error and discard it — it indicates intentional user cancellation, not a system failure.

Can I use the Web Share API over HTTP in production?

No. The Web Share API requires a secure context (https:// or localhost). Attempting to invoke it over HTTP results in NotAllowedError or the method being absent entirely, depending on the browser.