This repo is a living guide on reselling domains with fly for SaaS app builders.
Fly is launching APIs for programatic domain registration and DNS management. You can use these APIs to integrate custom domain reselling into your app. Domains are registered by fly (which means free WHOIS privacy for your customers) and can be transferred out anytime.
We are offering registration and renewal at cost. We charge your fly account immediately following a successful registration, transfer, or renewal. It's up to you to set your prices and bill your customers.
The implementation will vary from app to app, but here’s the gist:
1. build a domain search and registration UI into your app
The custom domain ux is totally up to you. At a minimum you'll need a page that lets your customers search for a domain and view availability with a button to buy a domain. Behind the scenes, you'll use our api to search and register the domain.
2. handle registration
When a domain is registered you'll want to record it in your database. This will let you manage domains without calling out to our API as well as to map requests for a custom domain to the right customer later on.
3. configure DNS
Once the domain is registered you can add the necessary DNS records to point the new domain to your app. The most common setup will be:
- an A record at the zone apex with your fly app's IPv4 address
- an AAAA record at the zone apex with your fly app's IPv6 address
- optionally an A record at
www
with your fly app's IPv4 address - optionally an AAAA record at
www
with your fly app's IPv6 address
4. configure SSL certicicate
Next you'll create an SSL certificate for the new domain. The DNS records from earlier are used for validation.
5. handle requests to the custom domain
Once DNS and SSL is configured, requets to the new domain will reach your web app. Lookup the customer by matching the HOST header to the domain's record in your database
Inevitability your customers will need to manage custom DNS records for their domain, such as mail servers or Google domain verification records. You can use the DNS management portal api to create minimal branded UIs to let your customers create and modify DNS records themselves. To use it, you'll use our API to generate a signed url which will render the portal for a limited time.
To use it, you first need to create a portal using the createDnsPortal mutation. It's helpful to name the portal and lookup on demand as opposed to storing the ID.
To create a session for a customer
- lookup the domain and portal ids using your organization slug, portal name, and domain name. Example
- create a session using the portal id and domain id Example
- redirect the customer to the url returned from the createDnsPortalSession mutation.
The fly API is built on GraphQL. If you're not familiar, take a look at the official docs.
The GraphQL api endpoint is available at
https://api.fly.io/graphql
You'll use a personal access token to authenticate your fly user to the api. You can create one in the fly dashboard or get your current one with flyctl auth token
The API will respond to any POST request with a well-formed JSON body. Most languages have clients, but anything that makes http requests will work. Here's an example using curl:
curl https://api.fly.io/graphql -X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer $(flyctl auth token)' \
-d ' {
"query": "query { viewer { id name email } }"
}'
Our GraphQL Playground is available at https://api.fly.io/graphql. This is a great tool for exploring the api and constructing queries that you can copy over to your app or scripts.
Most mutations that operate on an object require the object's global node id as an argument. Use the fields on query to lookup objects.
Query
query {
organization(slug: "your-org") {
id
name
slug
}
}
Response
{
"data": {
"organization": {
"id": "bjkZRb7BXzk36H381gwbX0z9bXRHZvM",
"name": "Your Org",
"slug": "your-org"
}
}
}
Query
query {
domain(name: "example.com") {
id
name
}
}
Response
{
"data": {
"domain": {
"id": "bjkZRb7BXzk36H381gwbX0z9bXRHZvM",
"name": "example.com"
}
}
}
The checkDomain
mutation returns domain availability and pricing.
Query
mutation {
checkDomain(input: {
domainName: "fly.io"
}) {
domainName
tld
registrationSupported
registrationAvailable
registrationPrice
registrationPeriod
transferAvailable
dnsAvailable
}
}
Response
{
"data": {
"checkDomain": {
"domainName": "fly.dev",
"tld": "dev",
"registrationAvailable": false,
"registrationSupported": true,
"registrationPrice": 1800,
"registrationPeriod": 1,
"dnsAvailable": true
}
}
}
The createAndRegisterDomain
mutation creates and registers a new domain in the provided organization. The input requires an organization node id which you can find on an Organization
object.
Query
mutation {
createAndRegisterDomain(input: {
organizationId: "bjkZRb7BXzk36H381gwbX0z9bXRHZvM",
name: "example.com"
}){
domain {
id
name
registrationStatus
dnsStatus
organization {
id
slug
}
}
}
}
Response
{
"data": {
"createAndRegisterDomain": {
"domain": {
"id": "vwvgDqn4kxLk58fsSgGAndh8z",
"name": "example.com",
"registrationStatus": "REGISTERING",
"dnsStatus": "pending",
"organization": {
"id": "bjkZRb7BXzk36H381gwbX0z9bXRHZvM",
"slug": "your-org"
}
}
}
}
}
An error response
{
"data": {
"createAndRegisterDomain": null
},
"errors": [
{
"message": "Validation failed: Name has already been taken",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"createAndRegisterDomain"
],
"extensions": {
"code": "UNPROCESSABLE"
}
}
]
}
The createAndTransferDomain
mutation creates a new domain in the provided organization and starts a transfer. The input requires an organization node id which you can find on an Organization
object and an authorization code.
Query
mutation {
createAndTransferDomain(input: {
organizationId: "bjkZRb7BXzk36H381gwbX0z9bXRHZvM",
name: "example.com",
authCode: "abc123"
}){
domain {
id
name
registrationStatus
dnsStatus
organization {
id
slug
}
}
}
}
Response
{
"data": {
"createAndTransferDomain": {
"domain": {
"id": "vwvgDqn4kxLk58fsSgGAndh8z",
"name": "example.com",
"registrationStatus": "TRANSFERRING",
"dnsStatus": "pending",
"organization": {
"id": "bjkZRb7BXzk36H381gwbX0z9bXRHZvM",
"slug": "your-org"
}
}
}
}
}
An error response
{
"data": {
"createAndTransferDomain": null
},
"errors": [
{
"message": "Validation failed: Name has already been taken",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"createAndTransferDomain"
],
"extensions": {
"code": "UNPROCESSABLE"
}
}
]
}
Use the domain
field to find a domain by name.
Query
query {
domain(name: "example.com") {
id
name
registrationStatus
dnsStatus
}
}
Response
{
"data": {
"createAndRegisterDomain": {
"domain": {
"id": "vwvgDqn4kxLk58fsSgGAndh8z",
"name": "example.com",
"registrationStatus": "REGISTERED",
"dnsStatus": "ready"
}
}
}
}
Domains are scoped to an organization. Use the domains
field on an organization to access it's domains.
Query
query {
organization(slug: "your-org") {
domains(first: 25) {
totalCount
nodes {
id
name
registrationStatus
dnsStatus
}
}
}
}
Response
{
"data": {
"organization": {
"domains": {
"totalCount": 1,
"nodes": [
{
"id": "vwvgDqn4kxLk58fsSgGAndh8z",
"name": "example.com",
"registrationStatus": "REGISTERED",
"dnsStatus": "ready"
}
]
}
}
}
}
The createDnsRecord
mutation creates a DNS record in the provided domain's hosted zone. The input requires a domain's node id which you can find on a Domain
object.
Query
mutation {
createDnsRecord(
input: {
domainId: "vwvgDqn4kxLk58fsSgGAndh8z"
type: A
rdata: "1.2.3.4"
name: "www"
ttl: 300
}
) {
record {
id
name
fqdn
type
ttl
rdata
}
}
}
Response
{
"data": {
"createDnsRecord": {
"record": {
"id": "P4XYgBwwqqKHwKVmGeaznAgGu6D28nV",
"name": "www",
"fqdn": "www.example.com.",
"type": "A",
"ttl": 300,
"rdata": "1.2.3.4"
}
}
}
}
The createDnsPortal
mutation creates a DNS portal in the provided organization. The input requires an organization's node id which you can find on an Domain
object. Portals can optionally be named to make lookups easier.
There are several options for customizing the portal:
- Return URL - use the
returnUrl
andreturnUrlText
input fields to customize the return button. If not provided no return button will be displayed. - Support URL - use the
supportUrl
andsupportUrlText
input fields to customize the support button. If not provided no support button will be displayed. - Title - customize the page title with the
title
input field. Defaults to"{organization name} DNS"
if omitted. - Colors - customize the primary color (nav bar background color) and the accent color (link and button color) with the
primaryColor
andaccentColor
input fields. Defaults to a bland but functional gray and blueish theme.
Query
mutation {
createDnsPortal(
input: {
organizationId: "vwvgDqn4kxLk58fsSgGAndh8z"
name: "example-production"
title: "Example DNS Portal"
returnUrl: "https://example.com/customer/123"
returnUrlText: "Back to Example"
supportUrl: "https://help.example.com/dns"
supportUrlText: "Help"
}
) {
dnsPortal {
id
name
}
}
}
Response
{
"data": {
"createDnsPortal": {
"dnsPortal": {
"id": "P4XYgBwwqqKHwKVmGeaznAgGu6D28nV",
"name": "example-production"
}
}
}
}
The createDnsPortalSession
mutation creates a time-limited session for the given dns portal and domain. The input requires a DNSPortal
node id and a Domain
node id. The response object contains the portal session URL.
You can optionally customize individual sessions with the title
, returnUrl
, and returnUrlText
input fields. If not provided the portal's default values will be used.
Query
mutation {
createDnsPortalSession(
input: {
dnsPortalId: "60RjyPqOlGVVVuDjAdsJp5b"
domainId: "VJMgmOfdDA45N4kT809"
}
) {
dnsPortalSession {
id
url
}
}
}
Response
{
"data": {
"createDnsPortalSession": {
"dnsPortalSession": {
"id": "G19dsfdfAZPjGVb42aHVOLmO4bOwlZ1iNYkggM",
"url": "https://portal.fly.io/dns/44227101de820cebae419f804838753e6b94567bcf6f951272b0d5f9ac31735d"
}
}
}
}
Creating a DNS Portal Session requires a DNSPortal node id and a Domain node id. You can look both of those up as needed using the organization slug, portal name, and the domain name.
Query
query {
organization(slug: "example-org") {
dnsPortal(name: "example-production") {
id
}
}
domain(name: "example.com") {
id
}
}
Response
{
"data": {
"organization": {
"dnsPortal": {
"id": "60RjyPqOdfgdlGVVVuDjAJp5b"
},
"domain": {
"id": "jwjGDMyzsdfsdex2eOhYV0"
}
}
}
}