Skip to content
LockShip

Guide · HTTPS and certificates · Updated

HTTPS for vibe-coded apps: redirects, HSTS, mixed content and certificates that won’t issue

HTTPS feels like the one thing you never have to think about anymore. Your builder publishes the app, there’s a padlock, done. And most of the time that’s true. Then you connect your own domain, or put Cloudflare in front, or a friend types the address without the www, and suddenly someone sees a full-page warning instead of the thing you built. This guide is the plain version of what can go wrong between a visitor and your app, how to check each piece yourself in a few minutes, and how to fix it on the hosts vibe-coded apps actually run on.

New to this? Start with our vibe coding security checklist, eleven checks for any AI-built app. HTTPS is the floor everything else stands on, so it’s worth ten minutes even if your app looks fine.

Why this got more urgent in October

On 21 October 2026, Google announced that Chrome now “explicitly asks users for permission before making an insecure HTTP connection,” for all users, by default. It had said a year earlier that this would arrive with Chrome 154. Chrome tries HTTPS first, and if a public site doesn’t answer on HTTPS, the visitor gets a warning and has to click through before they ever see your page.

The part that catches people is redirects. Chrome’s own guide for site owners spells it out: if any step on the way to your HTTPS page doesn’t support HTTPS, the warning shows, even though the visitor ends up somewhere safe. Its example is exactly the setup a lot of us have: http://example.com redirects to http://www.example.com, the bare domain has no HTTPS at all, and visitors who type example.com see a warning. Before October, nobody noticed that hop. Now your visitors do.

The other reason is boring and real. Without HTTPS, anyone on the same café Wi-Fi or a meddling network can read what your users send and change what they see. Netlify’s docs put it simply: free Wi-Fi services can inject ads into your pages. If your app has a login, a form or a checkout, plain HTTP isn’t an option.

What we tested, and how

Every command on this page we ran ourselves on 25 October 2026. The certificate and HSTS commands ran against lockship.dev. For redirects and mixed content we built a small local setup: a plain HTTP server and an HTTPS server on our own machine for a made-up domain, vibe-lab.example, opened in headless Chrome 155. One page loaded an image, an image through srcset, a script and a fetch() call, all over http://, so we could see what Chrome really does with each one. We didn’t test against Vercel, Netlify, Lovable, Replit or Cloudflare accounts. What we say about them comes from their docs, linked at the bottom, and your own curl output beats anything here.

The eight checks

  1. Try the four addresses a visitor can type
  2. Send every http:// address to https:// in one hop
  3. Add HSTS, and leave preload for later
  4. Find the http:// links hiding inside your pages
  5. Read your certificate: which names, and until when
  6. When the certificate won’t issue on your custom domain
  7. If Cloudflare sits in front, use Full (strict), never Flexible
  8. Keep renewals working after launch

01

Try the four addresses a visitor can type

Your app has at least four front doors: with and without www, each over http:// and https://. People type all four, old links use all four, and your builder probably only showed you one. Run this from a terminal (it works on a Mac and Linux, and in WSL on Windows):

Check all four addresses
# Put your own domain in D. Each address prints its status and where it sends you.
D=YOUR-APP.com
for u in http://$D http://www.$D https://$D https://www.$D; do
  echo "== $u"
  curl -sIL --max-time 10 --max-redirs 5 "$u" | grep -iE "^(HTTP/|location:)"
done

The -I asks for headers only and -L follows each redirect, so you see every hop. Here’s the HTTPS half on lockship.dev:

lockship.dev, 25 October
== https://lockship.dev
HTTP/2 200
== https://www.lockship.dev
HTTP/2 308
location: https://lockship.dev/
HTTP/2 200

That’s what you want everywhere: every address ends at 200 on one https:// address, and anything that starts on http:// jumps straight to https:// on its first hop. A hop that stays on http:// is the problem from the top of this page. This is what it looked like in our test setup:

Our test setup: apex to www over plain HTTP first
== http://vibe-lab.example
HTTP/1.1 308 Permanent Redirect
location: http://www.vibe-lab.example/
HTTP/1.1 308 Permanent Redirect
location: https://www.vibe-lab.example/
HTTP/1.1 200 OK

The visitor ends up on HTTPS, but the first hop was plain HTTP, and that’s what Chrome now warns about. Other things to look for: an address that times out or says Could not resolve host (no DNS record for it, which is fine as long as you never link to it), and a curl error about a certificate, which is check 5.

02

Send every http:// address to https:// in one hop

The rule is simple: an http:// request should get a permanent redirect (301 or 308) to the same path on https://, on the same host, in one step. Changing www to no-www, or the reverse, can happen after that, on HTTPS. OWASP’s TLS cheat sheet recommends exactly this pair: a permanent redirect for people who type the domain, backed by HSTS so the browser doesn’t try HTTP next time.

Where you set it depends on your host, and on most of them you don’t set it at all:

  • Vercel (v0 apps and most Next.js apps). Its encryption docs say the CDN “automatically forwards any HTTP requests to your deployment to HTTPS using the 308 status code,” and that this redirect “can’t be disabled.” So if your curl shows a plain HTTP hop on a domain you host at Vercel, something else is most likely answering first, like a proxy, an old DNS record, or a registrar’s forwarding service.
  • Netlify. Netlify issues the certificate on its own once your DNS points at it. If the http:// address still answers 200 after that, Netlify’s redirect docs show protocol-specific rules, with the warning that “http and https need separate rules”:
Netlify _redirects
# _redirects, only if the http:// address still answers 200 on Netlify
http://www.YOUR-APP.com/*  https://www.YOUR-APP.com/:splat  301!
http://YOUR-APP.com/*      https://www.YOUR-APP.com/:splat  301!
  • Lovable and Replit. Both issue the certificate for your custom domain themselves. We couldn’t find either documenting its HTTP redirect for published apps, and the same goes for Bolt’s hosting, so trust your curl. If one is missing, ask the host’s support before adding your own redirect in app code: a redirect that sits behind a proxy and reads the wrong header can loop. Lovable’s custom domain docs do one useful thing for you: when you connect a root domain, the option to redirect www to it is on by default, so both names get set up.
  • Cloudflare in front of anything. Cloudflare’s Always Use HTTPS setting redirects at the edge, and Cloudflare recommends not also redirecting at your origin. Read check 7 before you touch it.

Then make sure both the bare domain and www are added to your project at the host. That way the host gets a certificate for both and can do the www redirect on HTTPS. A common way to end up with a two-hop chain is a registrar’s “domain forwarding” feature doing the www redirect over plain HTTP, and Chrome’s guide names domain forwarding services specifically. Point the DNS at your host instead and let it handle both names.

03

Add HSTS, and leave preload for later

A redirect protects the second visit only if the browser remembers it. That’s HSTS: a Strict-Transport-Security header that tells the browser to use only HTTPS for your domain for a while, even when someone types http://. Chrome’s guide adds that HSTS “takes precedence” over its new automatic upgrades and that, with HSTS, Chrome won’t fall back to HTTP. Check whether you send it:

Do you send HSTS?
curl -sI https://YOUR-APP.com | grep -i strict-transport-security

A sensible value, the same one we use in our headers guide:

HSTS header
Strict-Transport-Security: max-age=63072000; includeSubDomains

max-age=63072000 is two years. includeSubDomains covers every subdomain too, so leave it out if you have one that can’t do HTTPS yet, like an old blog on another host. Vercel’s docs say it sends max-age=63072000 on custom domains without you doing anything. On other hosts, the curl above tells you. If you need to set it yourself, our security headers guide has tested configs for Next.js, Vercel, Netlify and Cloudflare Pages.

Two details from MDN that save confusion. Browsers ignore the header when it arrives over plain HTTP, so sending it on the redirect does nothing. It only counts on HTTPS responses. And you can undo it later only over HTTPS, by sending max-age=0.

Leave preload out for now. hstspreload.org, which runs the list, is blunt about it: “While HSTS is recommended, HSTS preloading is not recommended,” and removal “tends to be slow and painful.” Once you’re on it, a subdomain that can’t do HTTPS stops working for everyone. There’s a nice exception: Google’s registry says the .dev top-level domain “is included on the HSTS preload list,” and says the same for .app. If your app lives on one of those, browsers already insist on HTTPS before the first visit.

04

Find the http:// links hiding inside your pages

This is the one that bites apps that were “already on HTTPS.” Your page loads over HTTPS, but somewhere inside it there’s an http:// address: an API base URL in an environment variable, an image someone pasted, a widget script from an old tutorial. That’s mixed content, and MDN explains what browsers do now: images, audio and video get upgraded to HTTPS automatically, and pretty much everything else is blocked.

Here’s what Chrome 155 printed in the Console on our test page:

Chrome 155 Console, our test page
Mixed Content: ... requested an insecure element 'http://img.vibe-lab.example/logo.png'.
  This request was automatically upgraded to HTTPS
Mixed Content: ... requested an insecure image 'http://img.vibe-lab.example/srcset.png'.
  This request has been blocked; the content must be served over HTTPS.
Mixed Content: ... requested an insecure script 'http://cdn.vibe-lab.example/widget.js'.
  This request has been blocked; the content must be served over HTTPS.
Mixed Content: ... requested an insecure resource 'http://api.vibe-lab.example/data'.
  This request has been blocked; the content must be served over HTTPS.

Read that list as what would break in a real app. The plain image showed up, because Chrome quietly asked for it over HTTPS and our server answered. The same image through srcset stayed blank. The script never ran. And the fetch() to the API failed with nothing but “Failed to fetch” in your code, which is exactly how a mixed-content API call looks from inside an app: the login button that does nothing, the dashboard that never loads, and only on the live site, because on localhost everything is HTTP and nothing is mixed.

Find them in your code first. From your project folder:

Find hard-coded http:// addresses
# Run from your project folder: hard-coded http:// addresses in code, styles and env files
grep -rnE "http://[a-zA-Z0-9]" \
  --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" \
  --include="*.vue" --include="*.svelte" --include="*.html" --include="*.css" \
  --exclude-dir=node_modules --exclude-dir=.next --exclude-dir=dist --exclude-dir=.git . \
  | grep -vE "localhost|127\.0\.0\.1|www\.w3\.org"
grep -HnE "=[\"']?http://" .env* 2>/dev/null

On a small test project, that found all four kinds:

Output on our test project
./src/api.ts:1:const API = "http://api.example.com/v1";
./src/App.tsx:1:<img src="http://cdn.example.com/a.png">
./src/s.css:1:.a{background:url(http://img.example.com/b.png)}
.env.production:1:VITE_API_URL=http://api.example.com

Check your host’s environment variables too, not only the .env files on your laptop. A VITE_API_URL or NEXT_PUBLIC_API_URL set to http:// in the dashboard is the classic one. Then open your live site with DevTools on the Console tab, click through every screen, and type “Mixed Content” in the filter. If you’d rather hand the fix to your builder:

Prompt for your AI builder
Find every hard-coded http:// address in this project: API base URLs, image and font URLs, script tags, CSS url() values, iframes, and environment variables like VITE_API_URL or NEXT_PUBLIC_API_URL. Change each one to https://, or to a relative path if it points at this same site. Leave localhost addresses alone. Don't change anything else, and list every file and line you changed.

There’s also a safety net: the upgrade-insecure-requests directive in a Content Security Policy. On our test page, adding it made all four requests go over HTTPS, and the Console went quiet:

CSP safety net
Content-Security-Policy: upgrade-insecure-requests

It only worked because our test server also answered on HTTPS. MDN warns that if the resource isn’t available over HTTPS, “the request will fail without any fallback to HTTP.” So it’s a net, not a fix: change the addresses, then keep the directive. Our starter CSP in the headers guide already includes it.

05

Read your certificate: which names, and until when

Your browser’s padlock tells you the certificate is fine for the address you’re on right now. It doesn’t tell you whether it covers the other name, who issued it, or when it ends. This does:

Read the certificate
D=YOUR-APP.com
echo | openssl s_client -connect $D:443 -servername $D 2>/dev/null \
  | openssl x509 -noout -issuer -enddate -ext subjectAltName
lockship.dev, 25 October
issuer=C=US, O=Let's Encrypt, CN=YR2
notAfter=Jan 12 12:34:27 2027 GMT
X509v3 Subject Alternative Name:
    DNS:lockship.dev

Three things to read. Subject Alternative Name is the list of names the certificate covers. Run it once for YOUR-APP.com and once for www.YOUR-APP.com. Each must appear on the certificate it gets, or visitors on that name get a full-page warning. On lockship.dev each name has its own certificate, which is fine. notAfter is the expiry date. issuer tells you who your host used, which matters for CAA in check 6.

For a yes-or-no answer you can put in a script or a reminder:

Ends within 14 days?
# Prints "Certificate will expire" if it ends within 14 days (1,209,600 seconds)
echo | openssl s_client -connect $D:443 -servername $D 2>/dev/null \
  | openssl x509 -noout -checkend 1209600

Don’t be surprised by short dates. The browser and certificate industry’s Baseline Requirements cut the longest a public certificate can last to 200 days from 15 March 2026, 100 days from March 2027 and 47 days from March 2029. Ours runs 90 days, from 14 October 2026 to 12 January 2027. Short is fine when your host renews automatically. It just means a renewal that silently stopped working catches up with you within weeks, not a year later.

If curl or openssl complains instead of printing dates, the error usually names the problem. No alternative certificate subject name matches means the certificate is for a different name, often the host’s own name because your domain isn’t fully connected yet. Self-signed means nobody vouches for it. Unable to verify the first certificate means the server forgot the middle certificate in the chain. Some browsers fill that gap themselves, so it can look fine on your laptop while scripts, apps and other devices refuse to connect. Our headless Chrome on Linux refused it outright.

06

When the certificate won’t issue on your custom domain

On every host in this guide, the certificate for your domain is automatic. You don’t buy or upload anything. To issue one, the host has to prove to a certificate authority that it controls your domain, and that only works when your DNS points straight at the host and nothing else answers for it. When a domain sits on “pending” or “failed” for hours, it’s almost always one of these. Look first:

What your DNS really says
D=YOUR-APP.com
dig +short A $D
dig +short AAAA $D
dig +short CNAME www.$D
dig +short CAA $D

dig comes with Mac and Linux. If you don’t have it, Google’s online Dig tool shows the same records. Then go down this list:

  • The records don’t point at your host yet. Each host shows the exact A or CNAME values in its domain settings. Old values from a previous host, or a parked page at the registrar, keep the check failing. Netlify’s troubleshooting page also reminds you that old records have to expire from caches first (their TTL).
  • A leftover AAAA record. An IPv6 address from a previous host sends part of the traffic, and the certificate check, somewhere else. Lovable and Replit both ask you to remove AAAA records on the domain you connect.
  • A CAA record that leaves your host out. CAA records say which certificate authorities may issue for your domain. Let’s Encrypt explains that with no CAA record any public authority may issue, so you only need to act if you have one. Vercel’s troubleshooting docs name CAA records as the most common reason a certificate fails to generate. Lovable uses three authorities and asks you to allow all of them:
CAA records that let your host issue
; Lets Let's Encrypt issue for this domain (Vercel, Netlify and Replit use it)
YOUR-APP.com.  CAA  0 issue "letsencrypt.org"
; Lovable asks you to allow all three of the authorities it uses
YOUR-APP.com.  CAA  0 issue "pki.goog"
YOUR-APP.com.  CAA  0 issue "ssl.com"
  • A proxy in front of the host. If Cloudflare’s orange cloud is on, the certificate authority talks to Cloudflare, not your host. Netlify says plainly that you need to turn that routing off before it can issue. Replit says the same, and Lovable has a separate setup option for it. Check 7 has the details.
  • Only one of the two names was added. Netlify’s docs note it can end up with a certificate for example.com and not for www.example.com, and that “Renew certificate” in Domain management usually fixes it. On Vercel, add both names to the project.

Per host, where to look:

  • Vercel: Settings, then Domains, shows each domain’s certificate status. Its troubleshooting page suggests Let’s Debug for a second opinion, and warns that a stale _acme-challenge TXT record from a previous provider can send the validation elsewhere.
  • Netlify: Domain management, then HTTPS. It asks for DNS to be pointed at Netlify before it can issue, and if you use Netlify DNS, DNSSEC has to be off at your registrar.
  • Lovable: Project, then Settings, then Domains. A domain moves from “Setting up” to “Live” once the certificate is issued, and “Stalled” or “Failed” has a Retry button, so you don’t need to remove and re-add it. Lovable asks you to contact support if nothing has been issued after 72 hours.
  • Replit: Publishing, then Domains. Replit only watches for your records for about six hours after you add the domain. If they weren’t right in that window, it shows “Failed” and stops checking, so remove the domain and add it again once the records are correct.

07

If Cloudflare sits in front, use Full (strict), never Flexible

Lots of people buy their domain at Cloudflare or move DNS there, and every record starts out with the orange cloud on, meaning Cloudflare sits in the middle of every request. That middle has its own HTTPS setting, the SSL/TLS encryption mode, and one value of it causes most of the trouble.

In Flexible mode, visitors talk to Cloudflare over HTTPS, but Cloudflare talks to your host over plain HTTP. Your host, rightly, redirects that to HTTPS. Cloudflare asks over HTTP again. Round and round, until the browser gives up with ERR_TOO_MANY_REDIRECTS. Cloudflare’s redirect-loop page describes exactly this, and its Flexible page says: “If your origin forces HTTPS by automatically redirecting HTTP requests to HTTPS, do not use Flexible mode.” Vercel documents that its HTTPS redirect can’t be turned off, so Flexible in front of Vercel loops, and in front of any host that forces HTTPS it does the same. Even when it seems to work, the second half of the trip is unencrypted.

You have two good options:

  • DNS only (grey cloud). Cloudflare keeps your DNS and your host handles HTTPS directly. Vercel’s proxy guide recommends connecting directly where possible, Lovable suggests the grey cloud when Cloudflare shows errors 1001 or 1003, and Replit asks for it outright. It’s the simplest setup for a small app.
  • Full (strict). If you want Cloudflare’s proxy, set the mode to Full (strict), so Cloudflare connects to your host over HTTPS and checks its certificate. Vercel’s guide says this too, and that Flexible “can cause a redirect loop.” On Lovable, connect the domain with the “Uses Cloudflare or similar proxy” option, which gives you a CNAME to your lovable.app address instead of the A record.

One trap when switching from proxied to DNS only: Vercel warns to first confirm that your host already has a real certificate for the name. A Cloudflare Origin CA certificate is only trusted by Cloudflare, not by browsers.

08

Keep renewals working after launch

Automatic renewal is automatic until something quietly breaks the check it depends on. The renewal runs months after you last thought about DNS, so the cause is usually a change you made for another reason. The ones to know:

  • Replit’s verification TXT record has to stay forever. Replit’s custom domain docs say it renews through that record, and that if you remove it, “your next certificate renewal will fail and your domain will eventually serve an expired certificate.” Tidying up DNS after setup is how people break it.
  • Turning the Cloudflare proxy back on, or adding a redirect or firewall rule in front of the host, can block the HTTP check certificate authorities use. Vercel asks that /.well-known/acme-challenge/ stays reachable for renewals as well as the first issue.
  • Deleting records you don’t recognise. Vercel warns that NS records delegating _acme-challenge to it are needed for wildcard renewals.
  • An uploaded certificate. Netlify notes it can’t renew a custom certificate for you. If someone set one up “to be safe,” switch back to the managed one.

And don’t count on an email warning. Let’s Encrypt stopped sending expiry emails on 4 June 2025. Put the 14-day check from check 5 in a weekly reminder or a cron job, or use a monitor that reads the certificate for you.

What a clean result means

All four addresses end on 200 on one HTTPS address, with no plain HTTP hop in the middle. HSTS is there on HTTPS. The Console shows no Mixed Content lines on any screen. Both names are on a certificate that ends more than two weeks from now. Nothing sits in front of your host in Flexible mode. If that’s you, Chrome’s new warning will never show for your app, and a café network can’t read or change what your users see.

Run check 1 again whenever you touch DNS, change hosts, or add a domain, and check 4 after any change to an environment variable or an API address. That’s when this breaks.

Where LockShip fits

We built LockShip to read your app from the outside, the way a stranger would. For HTTPS, here’s exactly what that covers. The scan loads the address you give it. If you type just the domain, it starts at https://. If the page it ends up on is plain HTTP, because there was no redirect or because a redirect sent it back down to HTTP, that’s the “Site is served over HTTP” finding, and it links to check 2. The scan doesn’t try your http:// address on its own. To test your redirect, paste the address with http:// in front: if it ends up on HTTPS, nothing is flagged.

On the HTTPS page it ends up on, it reads the certificate once. An expired certificate is a critical finding, one that ends within 14 days is high, and one browsers wouldn’t trust (wrong name, self-signed, missing middle certificate) is high, with the reason in plain words. If the certificate is so broken that the page won’t load at all, the scan reports that certificate as the finding instead of just stopping. You’ll need the DNS TXT option to prove you own the site in that case, because the file and the meta tag sit behind the broken certificate. It also checks whether your homepage sends HSTS, though not its max-age.

What it doesn’t do: it doesn’t look for mixed content, doesn’t compare the www and bare addresses, doesn’t read your DNS or CAA records, and doesn’t grade your TLS versions or ciphers. Checks 1, 4 and 6 are yours. The scan only sends GET and HEAD requests and never writes anything. A clean result means “nothing obvious from the outside right now,” not “safe.”

The summary is free: a score and the single worst thing we found. The $39 Launch Safe certificate includes every finding with a fix prompt you can paste into your builder, a PDF, and seven days of re-scans while you fix things. Monitor, at $9 a month, re-runs the scan every week, so a certificate inside its last 14 days shows up in your report before your visitors see a warning.

Want to see what a stranger’s browser gets from your app?

Paste your app’s address, prove you own it, and see the worst thing we find. The summary is free.

Run a free scan

Before you rely on a result, read the disclaimer.

Questions people ask

Do I need to buy an SSL certificate for my Lovable, Vercel or Netlify app?

No. Vercel, Netlify, Lovable and Replit all issue a free certificate for your custom domain and renew it for you, as long as your DNS points straight at them. Lovable’s docs say outright that a certificate bought from your registrar isn’t used by its hosting. Paying for one only makes sense in special cases, and Netlify notes that it can’t renew a certificate you upload yourself.

Why does Chrome say my site is not secure when it has a certificate?

Usually one of three things. The visitor reached a plain http:// address that doesn’t redirect, often the version with or without www. Or the page loads something over http://, which Chrome either upgrades, blocks, or flags. Or the certificate doesn’t cover the exact name in the address bar, for example it’s issued for www.yourapp.com and the visitor typed yourapp.com. Checks 1, 4 and 5 find each one.

What changed in Chrome in October 2026?

Chrome 154 turned on the Always Use Secure Connections setting for everyone. Chrome now tries HTTPS first, and when a public site doesn’t answer on HTTPS it asks the visitor before it opens the plain HTTP version. That includes HTTP steps in the middle of a redirect chain, so a domain that redirects to www over plain HTTP first can trigger the warning even though the final page is on HTTPS.

Why do I get ERR_TOO_MANY_REDIRECTS after putting Cloudflare in front of my app?

Almost always because Cloudflare’s SSL mode is Flexible. Cloudflare then talks to your host over plain HTTP, your host redirects that to HTTPS, Cloudflare asks over HTTP again, and so on forever. Set the mode to Full (strict), or switch the record to DNS only (the grey cloud), which is the simpler setup Vercel recommends and Replit asks for.

Should I add preload to my HSTS header?

Not as a first step. hstspreload.org, which runs the list, says HSTS is recommended but preloading is not, and that getting off the list tends to be slow and painful. Preload covers every subdomain you will ever create. If your app is on a .dev or .app domain, the whole TLD is already preloaded, so browsers insist on HTTPS without you doing anything.

Keep reading

Sources

We read each of these on 25 October 2026. Hosts change their defaults, so if something here disagrees with their docs, trust the docs and tell us at hi@lockship.dev. The Chrome results are our own, from headless Chrome 155 against a local test server for a made-up domain. We didn’t see Chrome’s new HTTP warning in that headless setup, so what we say about it comes from Google’s posts. We didn’t test on any host’s account, and we didn’t run any of this against anyone else’s app.