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:3000on a physical phone reports an insecure context even though the content is local - Custom hostnames —
.testor.localdomains do not inherit thelocalhostexemption; they need a trusted certificate - Android WebView and some enterprise Chromium builds — these do not honour the
localhostsecure-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:
Testing and Verification
Once HTTPS is running, run through this checklist on every target platform:
Desktop verification (Chrome/Firefox/Edge)
- Open
https://localhost:3000— confirm the padlock icon appears, no “Not Secure” warning - Open DevTools → Console, paste and run
validateSecureEnvironment()from Step 1 — confirmisSecure: trueandhasShare: true - Open DevTools → Security — confirm the certificate is issued by “mkcert” CA, not a self-signed unknown CA
Physical Android device
- Confirm the device is on the same Wi-Fi network as your development machine
- Navigate to
https://192.168.x.x:3000(replace with your LAN IP from Step 2) - 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.pemvia Settings → Security → Install a certificate - Run
validateSecureEnvironment()in Chrome for Android’s remote DevTools console
Physical iOS device
- Navigate to
https://192.168.x.x:3000in Safari - If Safari shows an untrusted certificate, AirDrop
rootCA.pemto the device and install it via Settings → General → VPN & Device Management, then enable full trust at Settings → General → About → Certificate Trust Settings - Confirm
isSecureContextistruebefore attempting share
Common Pitfalls
- Custom
.testdomain without TLS: Onlylocalhostand127.0.0.1have 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 -installfirst 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
mkcertcertificate covers the hostnames listed at generation time, not individual ports. - iOS gesture requirement: Calling
navigator.share()from insideasynccode 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.
Related
- Understanding Secure Context Requirements — what constitutes a secure context and why the API enforces it
- Step-by-step guide to testing the Web Share API on localhost — physical device setup and emulator configuration
- Browser Support Matrix for Web Share API — minimum versions, platform gaps, and known quirks
- Implementing navigator.canShare() for graceful fallbacks — payload validation before calling share
- QR code generation for cross-device sharing — fallback when native share is unavailable