Skip to Content
App Development, Web Development

Using Sites Secured by Laravel Herd with Expo

Austin Drummond
Austin Drummond
Director of Development

We have been subscribers of Laravel Herd since day 1. In fact, we were there in Nashville at Laracon in 2023) and witnessed the announcement live! We immediately recognized the value of a GUI for Laravel Valet, with a few extra goodies sprinkled on top.

One of the suggestions I made to Taylor immediately following the announcements was "It would be amazing if it could support NVM." Since it handles switching PHP version like breeze, I figured it would be the perfect setup for our team as avid users of NVM (and Node.JS). Safe to say, I was tired of brewing our PHP setups on macOS.

One nice thing Laravel Herd and Laravel Valet do behind the scenes is install the LaravelValetCASelfSigned.pem into the macOS Keychain, thus trusting all certificates signed with Valet's CA for any secured sites. When this feature was introduced, it allowed Developers to simply issue a certificate to setup a HTTPS connection for their local dev site without needing to type in their password. Previously, you would have to authenticate the action in the Keychain to trust each site individually. After doing this many times, it can get a little annoying to type in your password each time.

One of the painpoints we experienced early on when pairing our mobile apps (Expo, Swift, Kotlin, etc.) with a Laravel backend was invalid HTTPS certificates. Since iOS Simulators and Android emulators do not leverage the macOS Keychain, read on below to see how to setup your test devices to trust your Herd or Valet sites.

iOS Simulator

This is very easy to do on iOS simulators. First, make sure your favorite simulator is open and fully booted.

Next, locate your CA Certificate at one of the following locations:

  • Herd - ~/Library/Application Support/Herd/config/valet/CA/LaravelValetCASelfSigned.pem
  • Valet - ~/.config/valet/CA/LaravelValetCASelfSigned.pem

Now drag-and-drop that certificate file onto the simulator window. Nothing obvious happens on drop. However, your Herd/Valet CA should now automatically be trusted.

You can verify this by going to Settings > General > About > Scroll to the bottom > Certificate Trust Settings and you will see a toggle switch enabled for the Laravel Valet CA Self Signed CN.

This is the name of the certificate file for both Herd and Valet, as Herd is actually implements a GUI around slightly modified version (fork) of Larvel Valet.

One thing to note is anytime you download any new iOS SDKs or launch new simulator devices, you will need to repeat the above steps. This also includes the scenario when you "Erase All Content and Settings" for your sim.

Android Emulator for Native Apps

In Android land, it's a little more complicated to install Laravel Valet CA at the system level. Instead, we are just going to ship the CA with the app bundle when it's installed. The following instructions should work for any type of Android app, but we also show how to automate the process on Expo apps by creating a project plugin.

If you are using Expo, you should skip this section and go to the next section.

First, we are going to create a Network Security Configuration file at app/src/main/res/xml/network_security_config.xml with the following contents:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
  <base-config>
    <trust-anchors>
      <certificates src="@raw/valet" />
      <certificates src="system" />
    </trust-anchors>
  </base-config>
</network-security-config>

This config file essentially tells your app to trust the Laravel Valet cert in addition to the Android OS CAs. You can read more about this file here.

Next, we need to copy this CA file into our App bundle. Since we don't want to blindly copy the CA from our individual system into the repo, we can copy the CA at build time instead. This solution is a little more portable across team members.

As a reminder, locate your CA Certificate at one of the following locations:

  • Herd - ~/Library/Application Support/Herd/config/valet/CA/LaravelValetCASelfSigned.pem
  • Valet - ~/.config/valet/CA/LaravelValetCASelfSigned.pem

In your app's build.gradle, define the following:

android {
    // Your existing android configuration
}

def sourceFile = "${System.properties['user.home']}/Library/Application Support/Herd/config/valet/CA/LaravelValetCASelfSigned.pem"
def targetDir = "${projectDir}/src/main/res/raw"

tasks.register("copyToRaw", Copy) {
    from(sourceFile)
    into(targetDir)

    rename { fileName ->
        "valet" // must be lowercase, no special chars
    }
}

// Make sure it runs before you build the APK
preBuild.dependsOn(copyToRaw)

One caveat to this solution is your entire team should be using Herd vs. Valet. It might be a little weird to mix and match, but you could easily solve it with a symlink (an exercise left for the reader). You will also likely need to add raw/valet to your .gitignore, as this will be unique per team member.

Now register your network security configuration in your AndroidManifest.xml file.

<?xml version='1.0' encoding='utf-8'?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application 
        android:networkSecurityConfig="@xml/network_security_config">
              <!-- The rest of your manifest -->
    </application>
</manifest>

Finally, be sure to setup the emulator's DNS as noted below.

Android Emulator and Expo (with Prebuild)

If the title hooked you because you are using Expo, this is the section for you. Thankfully, the above iOS instructions apply to the Expo, as well. However, later versions of Expo introduced a new concept called Continuous Native Generation, which essentially means your iOS and Android project files will be generation when your app is compiled for the App Store. At Reusser, we also use Prebuild due to preferring to using TailwindCSS via NativeWind in our Expo apps.

Follow the below instructions to create a config plugin to ensure your Expo Android Dev Client app trusts your Laravel Herd CA.

First, create a file called plugins/network_security_config.xml

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true">
        <trust-anchors>
            <certificates src="@raw/valet" />
            <certificates src="system" />
        </trust-anchors>
    </base-config>
</network-security-config>

Next, create a plugin script at apps/plugins/trust-ca.js:

const { AndroidConfig, withAndroidManifest } = require("@expo/config-plugins");
const { Paths } = require("@expo/config-plugins/build/android");
const path = require("path");
const fs = require("fs");
const fsPromises = fs.promises;

const { getMainApplicationOrThrow } = AndroidConfig.Manifest;

const withTrustLocalCerts = (config) => {
  return withAndroidManifest(config, async (config) => {
    config.modResults = await setCustomConfigAsync(config, config.modResults);
    return config;
  });
};

async function setCustomConfigAsync(config, androidManifest) {
  // Get Laravel Valet's CA cert from ~/.config/valet/CA/LaravelValetCASelfSigned.pem
  let caExists = false;
  let src_ca_path = "";
  let eligiblePaths = [
    path.join(
      process.env.HOME,
      ".config",
      "valet",
      "CA",
      "LaravelValetCASelfSigned.pem",
    ),
    path.join(
      process.env.HOME,
      "Library",
      "Application Support",
      "Herd",
      "config",
      "valet",
      "CA",
      "LaravelValetCASelfSigned.crt",
    ),
  ];
  for (const path of eligiblePaths) {
    if (fs.existsSync(path)) {
      caExists = true;
      src_ca_path = path;
      break;
    }
  }

  if (!caExists) {
    return androidManifest;
  }

  const src_file_pat = path.join(__dirname, "network_security_config.xml");
  const res_file_path = path.join(
    await Paths.getResourceFolderAsync(config.modRequest.projectRoot),
    "xml",
    "network_security_config.xml",
  );

  const res_dir = path.resolve(res_file_path, "..");
  const res_extracas_path = path.join(res_dir, "..", "raw");
  const res_ca_path = path.join(res_extracas_path, "valet.pem");

  const mkdirs = [res_dir, res_extracas_path];
  for (const dir of mkdirs) {
    if (!fs.existsSync(dir)) {
      await fsPromises.mkdir(dir, { recursive: true });
    }
  }

  await fsPromises.copyFile(src_file_pat, res_file_path);
  await fsPromises.copyFile(src_ca_path, res_ca_path);

  const mainApplication = getMainApplicationOrThrow(androidManifest);
  mainApplication.$["android:networkSecurityConfig"] =
    "@xml/network_security_config";

  return androidManifest;
}

module.exports = withTrustLocalCerts;

The above script will first look for Herd, then fallback to Valet.

Lastly, register your new config plugin in your app.json or app.config.js file:

{
  "plugins": [
      "./plugins/trust-ca.js"
  ]
}

Now, your Android app will always trust your backend API proxied by Herd/Valet.

Finally, be sure to setup the emulator's DNS as noted below.

DNS Settings for Android Emulators

One last thing to note: you will likely need to configure your Emaultor's AndroidWifi network to be pointed at a custom DNS server that will resolve all *.test domains to your emulators loopback interface IP 10.0.2.2. In other words, we need to make sure the app is reaching Laravel Herd. This will automatically forward requests to your Mac's localhost. We have a DNS server specifically for our Android emulators avalable to our team via Tailscale.


We have over a decade of experience building and shipping mobile applications with all types of technologies, ranging from Swift/Objective-C & Kotlin/Jave all the way to Expo & React Native.

If you’d like help implementing these features or help with your mobile, reach out to our team today to get the support you need.