Freestyle Docs

Freestyle / Docs

Inbound TLS

Publish web apps, databases, mailboxes, and game servers at a public domain.

Use inbound rules to give services on your VM a public domain. Publish a web app or API over HTTPS, a Postgres database, an IMAP mailbox, or a Minecraft server. For services that handle their own certificates, use TLS passthrough.

You can use a free style.dev subdomain or a domain you own.

For connections a VM opens to an API, database, proxy, or another VM, see Outbound TLS.

Publish A Domain To A VM

This rule sends HTTPS requests for app.acme.com to port 8000 on the VM:

import { Freestyle } from "freestyle";

const freestyle = new Freestyle();

await freestyle.tls.rules.create({
  action: "allow",
  domain: "app.acme.com",
  source: { public: true },
  destination: { vmId: "vm-123456", port: 8000 },
});

Use an unused style.dev subdomain for a free domain with no DNS setup. For your own domain, verify ownership and configure DNS first. Single-label wildcards such as *.acme.com are also supported.

source: { public: true } allows public clients to connect. destination selects the VM and port. No additional firewall rule is needed for the edge to reach your VM.

protocol defaults to http; Freestyle terminates HTTPS for this rule. To authorize each HTTP request before it reaches your VM, add Forward Auth.

Publish A Minecraft Server

Set protocol: "minecraft" and the rule is served by the edge’s Minecraft front on 25565 instead of over HTTPS.

// Players dialing mc.acme.com reach vm-123456's Minecraft server.
await freestyle.tls.rules.create({
  action: "allow",
  domain: "mc.acme.com",
  protocol: "minecraft",
  source: { public: true },
  destination: { vmId: "vm-123456", port: 25565 },
});

Point the domain’s A and AAAA records at the edge, exactly as for a web domain (see DNS). Players type mc.acme.com with no port; no SRV record is needed while the server is on 25565.

The edge reads the server address out of the client’s handshake — the routing key, the way Host is over HTTP — and splices the session through. It terminates nothing: a minecraft rule takes no transform, and the name needs no certificate.

A status ping — the row in a player’s multiplayer list — never starts a stopped VM. While the VM is stopped the row reads Sleeping — join to start the server; while it is running the player sees the server’s own MOTD, player count and icon. Joining starts the VM.

Two settings on the guest, because the edge sits between the player and the server:

  • prevent-proxy-connections=false in server.properties, or an online-mode server rejects every login.
  • Minecraft: Java Edition only. Bedrock Edition is a different protocol over UDP.

Publish A Port With Your Own Certificate

Set protocol: "tcp" and the edge matches the name in the client’s ClientHello and splices the connection to the VM without terminating it. Your VM answers the handshake with its own certificate.

// Clients dialing secure.acme.com reach vm-123456 on 8443, TLS end to end.
await freestyle.tls.rules.create({
  action: "allow",
  domain: "secure.acme.com",
  protocol: "tcp",
  source: { public: true },
  destination: { vmId: "vm-123456", port: 8443 },
});

Point the domain’s A and AAAA records at the edge, exactly as for a web domain (see DNS). Clients dial 443, the same port an http rule is served on; the SNI is what selects the rule. Serve the certificate from the VM — the platform issues none for the name, and a client that offers no SNI never reaches the rule.

Passthrough is what you reach for when the session is yours to end: mutual TLS your server verifies, a protocol over TLS that is not HTTP, or a certificate you issue yourself. What it gives up is everything the edge would otherwise do:

  • No transform. The edge holds no key to the session.
  • No HTTP/3. There is no TCP connection to splice.
  • Public ingress only — { public: true } -> { vmId, port }.
  • One name, one rule. A name published for passthrough is not also served as HTTPS; on 443 the more specific rule wins, and an exact tcp rule beats a *.acme.com http one.

Publish A Mailbox

Set protocol: "imap" and the edge serves the name on 993, terminates the TLS, and forwards plain IMAP to your VM.

// Mail clients dialing mail.acme.com on 993 reach dovecot on vm-123456:143.
await freestyle.tls.rules.create({
  action: "allow",
  domain: "mail.acme.com",
  protocol: "imap",
  source: { public: true },
  destination: { vmId: "vm-123456", port: 143 },
});

Point the domain’s A and AAAA records at the edge (see DNS). The name is issued a certificate through the same ACME pipeline an http rule uses, so your VM runs an unencrypted IMAP server on 143 and holds no key material.

Users configure their mail client with the domain as the IMAP server, port 993, SSL/TLS. Accounts and passwords are your IMAP server’s — the edge reads no part of the session past the handshake and injects nothing, so an imap rule takes no transform.

To terminate the TLS yourself, set protocol: "imaps" instead and land the rule on 993:

await freestyle.tls.rules.create({
  action: "allow",
  domain: "mail.acme.com",
  protocol: "imaps",
  source: { public: true },
  destination: { vmId: "vm-123456", port: 993 },
});

The edge then matches the SNI and splices, and your server answers the handshake with a certificate you obtain and renew. The platform issues none for the name.

Both are public ingress only, and 993 takes one rule per name: an exact imaps rule beats a *.acme.com imap one, the way 443 resolves tcp against http.

Publish A Database

Set protocol: "postgres" and the rule is served by the edge’s Postgres front on 5432.

// Clients connecting to db.acme.com reach vm-123456's Postgres on 5432.
await freestyle.tls.rules.create({
  action: "allow",
  domain: "db.acme.com",
  protocol: "postgres",
  source: { public: true },
  destination: { vmId: "vm-123456", port: 5432 },
});

Point the domain’s A and AAAA records at the edge, exactly as for a web domain (see DNS). Connection strings use sslmode=require or stricter:

psql "postgresql://app@db.acme.com/prod?sslmode=verify-full"

The edge terminates TLS at the domain and forwards the session to the VM’s Postgres port. Your own server runs the startup exchange and authenticates the client; the edge reads nothing past the handshake, so a postgres ingress rule takes no transform. The name needs a certificate, so it must be one the platform can serve — the same rule as an HTTPS domain.

An unencrypted connection (sslmode=disable) is refused: it carries no server name, and the name is what selects the rule.

Source And Destination

For public ingress, use source: { public: true } and destination: { vmId, port }. Both ends are required. A destination names one VM and its listening port, never an entire VPC.

A TLS rule grants a route; it does not block other access. The edge reaches the VM through its standing platform grant, independently of your firewall rules.

Declaring Rules With A VM

In vms.create, omit the destination’s vmId to route to the VM being created:

const { vm } = await freestyle.vms.create({
  firewall: { rules: [] },
  tls: {
    rules: [{
      action: "allow",
      domain: "app.acme.com",
      source: { public: true },
      destination: { port: 8000 },
    }],
  },
});

Exactly one end must omit its identity. If an inline rule is invalid, the whole create fails and no VM is made. Use tls.rules.create for a rule between existing resources.

Update A Route

update keeps the rule’s ID and creation time and replaces the full rule:

await freestyle.tls.rules.update("tls-123456", {
  action: "allow",
  domain: "app.acme.com",
  source: { public: true },
  destination: { vmId: "vm-123456", port: 9000 },
});

List, Get, And Delete

// Every rule in your account, newest first.
const { rules } = await freestyle.tls.rules.list();

// The rules that apply to one VM: those naming it, plus those naming a
// private network it is on.
const { rules: applied } = await freestyle.tls.rules.list({ vmId: "vm-123456" });

// Or the rules naming one network.
await freestyle.tls.rules.list({ vpcId: "vpc-backend" });

const rule = await freestyle.tls.rules.get("tls-123456");

await freestyle.tls.rules.delete("tls-123456");

From The CLI

freestyle tls create --domain app.acme.com --from public --to vm=vm-123456,port=8000
freestyle tls create --domain db.acme.com --protocol postgres \
  --from public --to vm=vm-123456,port=5432
freestyle tls create --domain mc.acme.com --protocol minecraft \
  --from public --to vm=vm-123456,port=25565
freestyle tls create --domain mail.acme.com --protocol imap \
  --from public --to vm=vm-123456,port=143
freestyle tls create --domain secure.acme.com --protocol tcp \
  --from public --to vm=vm-123456,port=8443
freestyle tls list --vm vm-123456
freestyle tls update tls-123456 --domain app.acme.com \
  --from public --to vm=vm-123456,port=9000
freestyle tls delete tls-123456

Each endpoint uses comma-separated key=value pairs (vm, port) or the bare word public. Omit --protocol for HTTP; the examples above select other protocols explicitly. Use --protocol imaps to serve a mailbox with the VM’s own certificate.

Lifecycle

A rule cannot outlive what it names. Delete a VM or a private network and the rules referencing it go too — the same cascade the firewall uses — so you never end up with a rule pointing at a machine that no longer exists. Every rule reports what it depends on:

const rule = await freestyle.tls.rules.get("tls-123456");
console.log(rule.dependencies);
// [{ kind: "vm", id: "vm-123456" }]

The dependency runs one way. Deleting a rule never touches the VMs or networks it named.

Limits

  • Wildcard domains such as *.acme.com match one label. A catch-all * cannot serve public ingress because no certificate covers every name.
  • tcp, imap, and imaps are public ingress only and take no transform. Passthrough tcp also does not support HTTP/3.
  • IMAP clients connect on 993. Port 143 is only the backend destination for an imap rule; the edge does not accept cleartext IMAP there.
  • Postgres clients must use TLS. CancelRequest is not routed.
  • SOCKS5 is outbound only.
esc