evertap

Connect from your laptop

Create a database or bucket in the UI with New resource, or with the CLI on the machine running evertap or a paired laptop:

evertap create postgres blog

Either shows its connection details. Every database URL and bucket endpoint points at ev.internal on the engine's standard port:

ServiceIn the connection details
PostgreSQLpostgres://<user>:<password>@ev.internal:5432/<database>
MySQLmysql://<user>:<password>@ev.internal:3306/<database>
Redisredis://<user>:<password>@ev.internal:6379/0
RustFS bucketEndpoint http://ev.internal:9000, region us-east-1
Garage bucketEndpoint http://ev.internal:3900, region garage

The address is the same in every mode, for every version, and on every machine, so a project's .env does not change when you change how evertap is reached or work from another laptop. evertap env <name> prints the lines to paste:

DATABASE_URL=postgres://blog:…@ev.internal:5432/blog
AWS_ENDPOINT_URL_S3=http://ev.internal:9000
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=…
AWS_SECRET_ACCESS_KEY=…
S3_BUCKET=photos
S3_FORCE_PATH_STYLE=true

What makes ev.internal reach evertap depends on the mode:

  • In local mode, evertap itself listens at ev.internal on the machine it runs on, and setup already pointed the name there. Apps on that machine connect with nothing more to do.
  • In network and cloudflare mode, each laptop runs evertap connect, which listens at ev.internal and carries each connection to evertap. The rest of this page sets that up.

Pair the laptop

Install the binary on the laptop. It needs no Docker. Then pair it with evertap:

evertap login https://evertap.example.com

Give the address you open evertap at, such as https://evertap.example.com or http://192.168.1.20:8080. Without http:// or https://, an IP address, localhost, or a .local name gets http://, and any other name gets https://.

login prints a code and opens the approval page, <address>/device?code=…, in your browser. In a browser that is signed in to evertap, check that the code matches the one in your terminal, and approve it. On a laptop without a browser, open <address>/device on any signed-in device and type the code. The code expires in 10 minutes.

login saves the address and this laptop's token in ~/.config/evertap/config.json, readable by your user alone. Over plain http:// to another machine, it warns you: anyone watching that network can read the token and act as you. The laptop then appears under Clients in the UI, where you can revoke it. evertap logout revokes the token and forgets the address.

Run evertap connect

evertap connect

It listens on 127.77.0.1 at 5432, 3306, 6379, 9000, and 3900, and carries every connection over its own WebSocket to evertap's address. Keep it running while you work, in its own terminal; stop it with Ctrl+C.

The first time, point ev.internal at 127.77.0.1. connect prints the command until the line is there.

Linux

echo "127.77.0.1 ev.internal" | sudo tee -a /etc/hosts

Linux answers on all of 127.0.0.0/8, so the address needs nothing else.

macOS

macOS has only 127.0.0.1 on loopback, so connect first asks you to add the address:

sudo ifconfig lo0 alias 127.77.0.1 netmask 255.255.255.255
echo "127.77.0.1 ev.internal" | sudo tee -a /etc/hosts

The alias lasts until the Mac restarts. Run the ifconfig line again after a restart, before evertap connect.

Windows

There is no Windows build. Run the CLI and your apps inside WSL and follow the Linux steps. Windows apps outside WSL do not reach ev.internal there, because WSL passes only 127.0.0.1 on to Windows.

WSL writes a new /etc/hosts each time it starts, which drops the ev.internal line. To keep it, add this to /etc/wsl.conf in WSL, and restart WSL with wsl --shutdown from Windows:

[network]
generateHosts = false

Check it

evertap status                    # which evertap this CLI uses, and whether it is signed in
psql "$(evertap url blog)"        # connects through ev.internal

When a port is taken

If something on the laptop already holds one of the ports on every address, connect says which port, what likely holds it, and the command that shows it, such as sudo lsof -nP -iTCP:5432 -sTCP:LISTEN. The other ports keep working. Something that listens on 127.0.0.1 alone does not get in the way, because 127.77.0.1 is a different address.

  • --engine <service> carries only the services you name, such as evertap connect --engine postgresql --engine redis.
  • --host <address> listens on another address. Point ev.internal at it instead, since the connection details still say ev.internal. An address other machines can reach lets anyone there use your connections with your sign-in, leaving only each database's password in the way, and connect warns about it.

Databases you still run with Compose

Publish them on 127.0.0.1, which leaves 127.77.0.1 free:

services:
  db:
    image: postgres:18
    ports:
      - "127.0.0.1:5432:5432"

With "5432:5432", Compose listens on every address: while it runs, evertap connect cannot listen on 5432, and while evertap connect runs, that Compose file fails to start. The same goes for evertap itself in local mode.

Apps in containers

An app in a container does not reach ev.internal, just as it does not reach your laptop's localhost. An app that runs in Docker keeps its database in its own Compose file.

Connection pools

Each new connection opens a WebSocket to evertap, over TLS when evertap's address is https://, so opening one costs more than with a database on your laptop. Use a connection pool.

Connections can also drop: when evertap restarts, when your network changes, or when a proxy or tunnel in between restarts. evertap and evertap connect ping each other every 25 seconds and close both ends after two pings go unanswered, so your app sees the error instead of waiting. Have the pool check a connection before it uses it, and replace connections after about 30 minutes:

LibrarySetting
node-postgresnew Pool({ maxLifetimeSeconds: 1800 })
SQLAlchemycreate_engine(url, pool_pre_ping=True, pool_recycle=1800)
Go database/sqldb.SetConnMaxLifetime(30 * time.Minute)
DjangoCONN_MAX_AGE = 1800 and CONN_HEALTH_CHECKS = True
HikariCPmaxLifetime is 30 minutes by default
PrismaLeave pgbouncer=true out of the URL; see below

PostgreSQL connections go through PgBouncer in transaction mode, so the server connection behind yours can change between transactions. Prepared statements that drivers make work, as evertap's PgBouncer keeps track of them, so Prisma needs no pgbouncer=true. Session state does not carry over: SET (use SET LOCAL inside a transaction), LISTEN, session advisory locks, and temporary tables last only until the transaction ends.

Buckets

Use the endpoint with path-style addressing, since <bucket>.ev.internal does not resolve. No AWS SDK reads S3_FORCE_PATH_STYLE, so pass it in code: forcePathStyle: true in the AWS SDK for JavaScript, or Config(s3={"addressing_style": "path"}) in boto3.

Presigned URLs point at ev.internal too, so they open only where evertap connect runs, or on the machine running evertap in local mode. A page on http://localhost can upload to them from the browser, as every bucket allows any origin; a page served over HTTPS cannot load an http:// address.

When a link has to open elsewhere, such as in a preview deployment's browser, on a phone, or in an outside service that fetches the file, turn on Reachable from anywhere on the bucket's page, or run:

evertap reachable photos          # evertap reachable photos --off turns it off

Objects still need a signature: this is not S3's public access, and a link without one is refused. It takes network or cloudflare mode, and an address of buckets' own, such as https://s3.example.com, that your proxy or tunnel serves. A tunnel that setup creates serves them at s3.<your domain> already. local mode does not offer it.

The bucket's endpoint stays at ev.internal, so the rest of your app works as before, and its .env gains one line:

S3_PUBLIC_ENDPOINT=https://s3.example.com

Sign links with a second client on that address, and keep your app's client on AWS_ENDPOINT_URL_S3. Signing happens in your app, so the second client makes no requests:

import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

// Only for signing links; every other call keeps to AWS_ENDPOINT_URL_S3
const signer = new S3Client({
  endpoint: process.env.S3_PUBLIC_ENDPOINT,
  forcePathStyle: true,
  // Otherwise the link carries the checksum of an empty body, and Garage refuses the upload
  requestChecksumCalculation: "WHEN_REQUIRED",
});

const url = await getSignedUrl(
  signer,
  new PutObjectCommand({ Bucket: process.env.S3_BUCKET, Key: "avatar.png" }),
  { expiresIn: 3600 },
);
import os

import boto3
from botocore.config import Config

# Only for signing links; every other call keeps to AWS_ENDPOINT_URL_S3
signer = boto3.client(
    "s3",
    endpoint_url=os.environ["S3_PUBLIC_ENDPOINT"],
    # boto3 reads AWS_DEFAULT_REGION, not AWS_REGION
    region_name=os.environ["AWS_REGION"],
    config=Config(signature_version="s3v4", s3={"addressing_style": "path"}),
)

url = signer.generate_presigned_url(
    "put_object",
    Params={"Bucket": os.environ["S3_BUCKET"], "Key": "avatar.png"},
    ExpiresIn=3600,
)
  • Links are path-style, https://s3.example.com/photos/avatar.png: one address cannot hold each bucket's name in its hostname.
  • Form uploads (a presigned POST) work with Garage buckets. RustFS does not take them.
  • Through a Cloudflare Tunnel, see its limits.

CI with an API key

A CI job cannot approve evertap login, so it uses an API key instead.

  1. In the UI, open Clients → New API key and name it. The key appears once, as EVERTAP_API_KEY=evertap_…: evertap keeps only its hash.
  2. Store it as a secret in your CI, and set EVERTAP_URL to the address you open evertap at. With both set, the CLI needs no login.
  3. In the job, create what the tests need, and carry connections with evertap connect as on a laptop:
curl -fsSL https://github.com/caru-ini/evertap/releases/latest/download/install.sh | sh
name="ci_${GITHUB_RUN_ID}"
evertap create postgres "$name" --temporary
export DATABASE_URL="$(evertap url "$name")"

echo "127.77.0.1 ev.internal" | sudo tee -a /etc/hosts
evertap connect --engine postgresql &
timeout 10 bash -c 'until echo > /dev/tcp/127.77.0.1/5432; do sleep 0.5; done'

# run the tests

evertap delete "$name" --yes

--temporary deletes the database after 7 days without a connection, in case the last step never runs. The CI runner has to reach evertap's address, so evertap on a home network needs a proxy or a tunnel with a public address, or a runner on that network.

An API key can do everything a paired CLI can with resources, including reading their data and deleting those that are not protected. It cannot approve sign-ins, create API keys, or revoke anything; those stay in the UI. Revoke a key under Clients when it leaks or you stop using it.

Edit on GitHub

The evertap name and logo are not licensed with the code (section 6 of the license). You may use them to refer to evertap, but not to name or brand your own product or service, or in a way that suggests evertap made or endorses it, without permission.

evertap is an independent project. It is not affiliated with, endorsed, sponsored, supported, or certified by the owners of the software it runs, and it uses their names only to say which software that is.

  • Postgres, PostgreSQL and the Slonik Logo are trademarks or registered trademarks of the PostgreSQL Community Association of Canada, and used with their permission.
  • MySQL is a registered trademark of Oracle and/or its affiliates.
  • Redis is a registered trademark of Redis Ltd. Any rights therein are reserved to Redis Ltd.
  • RustFS is a trademark of RustFS, Inc.
  • Other names, including Garage, may be trademarks of their respective owners.

On this page