> ## Documentation Index
> Fetch the complete documentation index at: https://docs.chargerdojo.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Connect a local endpoint

> Test an OCPI implementation that runs on localhost or inside a VPC: a tunnel today, or the evsim-connect connector, which forwards our requests without putting your service on the internet.

Your OCPI implementation runs on `http://localhost:8000`, or on a machine inside a VPC
that only your own network can reach. The tester is hosted, and it calls your endpoint
server to server. It cannot reach a laptop.

This is the normal state of a half-built integration, not a mistake you made. The fix that
works today is a tunnel: a small program you run next to your service that gives it a
temporary public HTTPS URL. You then use that URL as the base URL of a connection, exactly
as you would a partner's.

## What we do not ask you for

* No inbound firewall rule.
* No public hostname of your own.
* No TLS certificate on your side. The tunnel terminates HTTPS for you.

Your service keeps listening on plain HTTP on localhost. Nothing about it changes.

## Start a tunnel

Both of these give you a random HTTPS URL and print it in the terminal. Pick one. Replace
`8000` with the port your OCPI service listens on.

### cloudflared

```bash theme={null}
brew install cloudflared          # or see the Cloudflare install docs for your OS
cloudflared tunnel --url http://localhost:8000
```

It prints a line with a `https://<random-words>.trycloudflare.com` URL. That is your
public base URL. No account is needed for this kind of quick tunnel.

### ngrok

```bash theme={null}
brew install ngrok                # or see the ngrok install docs for your OS
ngrok config add-authtoken <your ngrok authtoken>
ngrok http 8000
```

It prints a `Forwarding` line with a `https://<random>.ngrok-free.app` URL. That is your
public base URL. ngrok needs a free account for the authtoken, and it needs it only once
per machine.

Leave the tunnel running for as long as you are testing. Both tools issue a **new random
URL every time you restart them**, so if you stop and start the tunnel, the connection you
created points at a URL that no longer exists.

## Check it from outside your machine

Before you create a connection, prove the tunnel reaches your service:

```bash theme={null}
curl -fsS "https://<your-tunnel-host>/ocpi/versions" \
  -H "Authorization: Token $(printf %s '<your token A>' | base64 | tr -d '\n')"
```

The `tr -d '\n'` is not decoration. GNU coreutils `base64` wraps its output every 76 characters,
so a token A longer than 57 bytes comes back with a newline in the middle and the header you send
is not the header you meant. It costs nothing on macOS, which does not wrap.

From OCPI 2.2 onwards the credentials token is Base64 encoded in the `Authorization` header, and
that is what we send, so testing with the encoded form is testing what will actually arrive. On
2.1.1 the token goes raw instead: `-H "Authorization: Token <your token A>"`.

If that answers, we can reach you too. If it does not, fix it here rather than in a run
report.

## The part that costs people an afternoon

The tunnel host has to be the base URL you register **and** the host your own responses
advertise.

OCPI is a discovery protocol. Our client reads your `/versions` response, follows the
`url` it finds there to your version details, and follows those `endpoints` URLs to every
module. If your service builds those URLs from its local configuration, it will hand back
`http://localhost:8000/ocpi/2.2.1/cdrs`. We refuse to call that. A loopback or private
address is blocked before the request leaves us, so nothing is sent anywhere.

You will see this in the report, on the check that tried to use the URL:

```
Refusing to call private/internal host: localhost
```

That line is our refusal, not an answer from your service, so read it as "you advertised a
URL only your own machine can reach" rather than as an HTTP failure.

Where it lands is simply the first advertised URL we cannot call. Registration reads your
`/versions`, follows the `url` for the version you chose, and then posts to the `credentials`
endpoint listed there:

* If the `url` in your `/versions` response is local, registration stops at the version details
  step. This is the usual case, because one base setting normally feeds everything.
* If only the endpoint URLs in your version details are local, registration stops at the
  credentials step.
* If only some module endpoints are local, registration finishes and those modules fail.

So set whatever your service uses as its public base URL (an environment variable in most
implementations) to the tunnel host before you start it, and confirm it with the `curl`
above. Every URL in the response should name the tunnel host.

## Point a connection at it

From here nothing is special. Create the connection with the tunnel URL as
`partnerBaseUrl` and register it, the same as any partner:

```bash theme={null}
curl -fsS -X POST "https://chargerdojo.com/api/v1/connections" \
  -H "Authorization: Bearer $EVRT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "partnerBaseUrl": "https://<your-tunnel-host>/ocpi",
    "ourRole": "EMSP",
    "theirRole": "CPO",
    "tokenA": "YOUR-TOKEN-A",
    "ocpiVersion": "2.2.1"
  }'
```

[Connect a partner endpoint](/guide/connect-a-partner) covers the fields, the registration
handshake, and the version choice. Everything on that page applies here.

## What this costs you

**While the tunnel is up, your service is on the public internet, and anyone who has the
URL can reach it.** There is no way around that: a tunnel works by making your service
reachable, and reachable means reachable by whoever holds the address.

The mitigations that actually exist:

* **The URL is random and temporary.** Nobody can guess
  `https://<random-words>.trycloudflare.com`, and it stops working when you stop the
  tunnel. This is real protection against strangers, and no protection at all against
  anyone you sent the URL to.
* **Our requests carry the token you configured**, and nothing else about the tunnel
  changes your authentication. If your service rejects requests without a valid OCPI
  token, it still rejects them through the tunnel. This is your actual access control. If
  your service does not check the token yet, the tunnel is the wrong time to find that out.
* **Stop the tunnel when the run finishes.** The exposure lasts exactly as long as the
  process does. Stopping it costs you nothing except a new URL next time.
* **Use throwaway data.** A development database with invented CDRs and test tokens, never
  a copy of production.

<Warning>
  The login features these tools offer for protecting a URL (ngrok's OAuth, Cloudflare
  Access) put a browser sign-in in front of your service. Our requests are not a browser
  and cannot sign in, so turning those on will block us along with everyone else. Treat
  your own OCPI token check as the control that matters, and keep the tunnel window short.
</Warning>

If none of that is acceptable to you, use the connector instead. It is the section below, and
it never puts your service on the internet.

## Speed

A conformance run is sequential and small. Each request crosses the internet twice more
than it otherwise would, which adds tens of milliseconds per request and changes no
verdict. A tunnel is fine for conformance.

A load run is different. It measures the tunnel as much as it measures you, so the numbers
it produces are not about your service. Do not size anything from a load run over a tunnel.

## The connector, which exposes nothing

<Note>
  The connector is in limited release and available on request. The tunnel described above is the
  path you can take today. Write to us if you want the connector for your evaluation.
</Note>

`evsim-connect` is a small binary you run next to your service. It opens an **outbound**
WebSocket to us and forwards the requests we send it to a local address you name. It needs no
inbound firewall rule, no public hostname and no TLS certificate, and your service is never
reachable from the internet at any point. It removes the trade-off the rest of this page
describes.

It is a pipe and nothing more. It holds no test suites, no checks and no verdicts: everything
that decides whether your implementation is correct stays on our servers. Connectors are part
of a paid plan.

### Get the connector

We build five binaries from one release job:

| Platform             | File                              |
| -------------------- | --------------------------------- |
| macOS, Apple silicon | `evsim-connect-darwin-arm64`      |
| macOS, Intel         | `evsim-connect-darwin-amd64`      |
| Linux, x86-64        | `evsim-connect-linux-amd64`       |
| Linux, arm64         | `evsim-connect-linux-arm64`       |
| Windows, x86-64      | `evsim-connect-windows-amd64.exe` |

Each release also carries a `checksums.txt`. Check your download against it before you run it.

There is no public download page yet, so [email us](mailto:ahoy@synergyboat.com) and we will
send you the build for your platform.

The binaries are not code signed yet, so the first run needs one extra step:

* **macOS** blocks an unsigned download. Right-click the file and choose Open, or run
  `xattr -d com.apple.quarantine ./evsim-connect`.
* **Windows** SmartScreen shows a warning. Choose "More info", then "Run anyway".
* **Linux** needs `chmod +x ./evsim-connect`.

### Pair it

Go to **Endpoints** and start **Add endpoint**. When it asks where the service is, answer
**Behind a local connector**, then choose **Pair a connector**. Name it after the machine it will
run on: that name is what every endpoint and every report calls it. The endpoint form stays open
behind the pairing, so nothing you have already typed is lost.

Once you have one connector, the **Local connectors** list at the foot of the Endpoints page
pairs any others.

Pairing gives you a token that starts with `evrtc_`. We do not keep it, so we cannot show it to
you twice. Copy it when it appears; if you lose it, revoke the connector and pair a new one.

### Run it

```bash theme={null}
evsim-connect --target http://localhost:8000 --token <pairing token>
```

It prints one line when it is up:

```
connected as target "oleg-emsp-local"
```

The name in that line is the connector's own: `--name` if you passed one, otherwise your
machine's hostname. It is not the name you typed when pairing, which is the one endpoints and
reports use.

On any machine other people can log into, and in CI, pass the token in the environment instead.
On the command line it is visible in your shell history and to anyone who can list processes:

```bash theme={null}
export EVSIM_CONNECTOR_TOKEN=<your pairing token>
evsim-connect --target http://localhost:8000
```

Change `--target` if your service is not on port 8000, and repeat `--target` if it answers on
more than one port.

The connector shows as connected on the Endpoints page within a few seconds. Go back to the
endpoint form, pick your connector, and give the base URL your connector forwards to. Register it
and run a suite exactly as you would against a partner.

### Where your connector will and will not send requests

Two rules, in this order, and both live in the connector rather than on our servers.

1. **It forwards only to the origins you named with `--target`.** A request for any other host or
   port comes back refused, and the refusal names what was asked for. Every loopback spelling of
   your own machine counts as the same place, in either IP version, which matters because your
   service will usually advertise the numeric form during the handshake even when you typed the
   name.
2. **The address has to be one your own network owns.** Loopback and the RFC 1918 private ranges,
   plus three that catch people out if they are left off: carrier-grade NAT (`100.64.0.0/10`),
   which is what Tailscale and other mesh VPNs hand out, so reaching your service over one works;
   link-local (`169.254.0.0/16`, `fe80::/10`); and the all-zeroes "this network" block, which on
   Linux dials loopback. Everything else is public. This is
   checked against the address the name actually resolves to, every time the connector dials, not
   once at startup, so a name that starts answering with a public address later does not get
   through. Requests that reuse an already open connection do not dial again, so the check is per
   connection rather than literally per request. Pass `--allow-public-target` if you deliberately
   want to forward somewhere public.

`--allow-public-target` is not a blanket waiver. Cloud instance metadata addresses
(`169.254.169.254` and four siblings on AWS, Alibaba Cloud and the AWS container agents) stay
refused with it on, because those addresses hand out the access keys of the machine your
connector runs on.

Because your connector dials your service, we never do. On this path we do not resolve or
connect to your address at all, which is why a private address is fine here and refused on the
tunnel path above.

### What you are trusting

Worth saying plainly, because it is the one thing no code on our side can check.

We send requests to your connector. Your connector decides where they go. The verdict in the
report is therefore only as good as the pairing token: anyone holding it can connect and answer
for your endpoint, and the answers they give would be graded as yours. Keep the token where you
keep your other secrets, and revoke it the moment you think it has leaked.

Revoking is immediate and reaches a running process: the connector disconnects within a second,
prints why, and its token stops working for good. One token also means one machine. Start the
same token somewhere else and the first connector is closed out, so pair a second connector for
a second machine rather than sharing one.

### What a connector does not do yet

* **Charging Journeys and load runs do not work over a connector.** They run in a separate
  worker process that cannot reach the socket your connector holds, so the app does not offer a
  connector-backed endpoint for a journey and the API refuses one if you ask it directly.
  Conformance runs are what a connector carries today.
* **A response body larger than about 6 MiB fails the request**, rather than being split across
  frames. The socket carries 8 MiB frames, and bodies travel Base64 encoded, which costs a third:
  on the default limit the usable body budget works out at 6,285,312 bytes. The refusal names that
  body budget and never the 8 MiB frame ceiling, deliberately, because quoting 8 MiB would send you
  looking for headroom that was never there.
* **A run does not wait for a connector to start.** If the connector is not running when you
  start one, the run fails immediately with `The connector "<name>" for this endpoint is not
  connected. Start it and try again.` Start the connector and run again.
* **A run that is already going does wait, briefly, for a connector that drops.** Your machine
  changing wifi or waking from sleep takes the socket down and the connector comes straight back
  on a new one. A check issued in that gap waits up to two seconds for the new connection and then
  travels over it, so a blip does not turn the rest of the run into failures your endpoint never
  caused. If nothing comes back in those two seconds, the run stops waiting and every remaining
  check fails by name, so a connector that is really gone never looks like a partner problem. The
  report records that the run crossed a reconnect either way.

## Next

1. [Connect a partner endpoint](/guide/connect-a-partner) to create and register the
   connection.
2. [Run a conformance suite](/guide/run-a-suite) against it.
