I've always believed the best way to really understand a technology is to push it in stupid, impractical directions and see what breaks. Inspired by Corey Quinn's claim that Amazon Route 53 is actually a database, I wondered: what's the opposite of that? If DNS can be used as a database, what's the most absurd way to implement a DNS server?
That thought led me to this project - using Stripe metadata as the datastore for a fully functional DNS server.
Metadata
When working with Stripe, metadata is a really important concept. It greatly simplifies linking Stripe's data model to your own application. Order ids, loyalty programme membership, referral scheme ids - Stripe doesn't know or care what these concepts in your application are, but allows us to store the info on core Stripe objects to simplify linking the two systems together.
You can use it to tag a customer with an internal ID:
"internal_user_id": "abcd1234"
Or track who triggered a refund:
"refunded_by": "support_team_bot"
Ultimately metadata is a simple key-value store where you can store arbitrary values on core Stripe objects. Customers, Subscriptions, Payment Intents, Products, etc - all the core Stripe objects have a metadata field, in which you can store up to 50 key-value pairs on each object. While Stripe metadata is just a key-value store, with some clever flattening and serialisation (using techniques like JSON.stringify()), it can be coerced into storing structured DNS records - if you're willing to abuse a few best practices along the way.
DNS records are essentially a mapping of record -> location, which sounds an awful lot like something suitable for key-value mapping.
Setting up the DNS server
For the DNS server, we'll use dns2. This gives us the ability to listen for DNS queries and craft responses:
const dns2 = require('dns2')
class StripeDnsServer {
constructor(options = {}) {
this.stripeClient = options.stripeClient || stripe
this.port = options.port || 5333
this.address = options.address || '0.0.0.0'
this.server = this._createServer()
}
_createServer() {
return dns2.createServer({
udp: true,
handle: this._handleRequest.bind(this),
})
}
start() {
this.server.listen({
udp: { port: this.port, address: this.address, type: 'udp4' },
tcp: { port: this.port, address: this.address },
})
return this
}
}
const dnsServer = new StripeDnsServer({ port: 5333 }).start()
This will listen on port 5333 (UDP and TCP) and pass incoming requests to a _handleRequest method, which we still need to define.
Structuring DNS records for metadata storage
When working with DNS, you'll typically have records like:
| Type | Name | Content | Preference |
|---|---|---|---|
| A | @ | 192.168.1.2 | |
| A | www | 192.168.1.3 | |
| MX | @ | mail01.google.com | 10 |
| CNAME | blog | blog.wordpress.com. |
Ideally, we'd want to represent this in a structured format like JSON:
{
"@": { "A": ["192.168.1.2"] },
"www": { "A": ["192.168.1.3"] },
"blog": { "CNAME": ["blog.wordpress.com"] },
"_mx": {
"MX": [{ "preference": 10, "exchange": "mail01.google.com" }]
}
}
Our first problem with mapping this to Stripe's metadata is that we've got more than just a simple key-value relationship here. Stripe metadata doesn't support complex structures, so arrays and nested data structures won't work out of the box. It's strictly key-value, where the value is a string (up to 500 characters).
That limitation doesn't stop us, it just means we have to flatten the data ourselves first using JSON.stringify() before storing it, and then re-expand it using JSON.parse() upon retrieval. We'll store the entire JSON structure as a string under a single metadata key, dns_records:
metadata.dns_records: '{"blog":{"CNAME":["blog.example.net."]},"www":{"A":["192.168.1.3"]},"@":{"A":["192.168.1.2"],"MX":[{"priority":10,"exchange":"mail1.example.com."}]}}'
To associate this with a domain, we add another metadata key: dns_domain = example.com.
Storing the data in Stripe
Where do we attach this metadata? Stripe Customer objects are perfect. We can create them via the API without needing payment methods or actual financial data. We're effectively using Stripe as a (very unusual) free-tier key-value database.
const customer = await stripe.customers.create({
name: 'DNS records for example.com',
metadata: {
dns_domain: 'example.com',
dns_records: '{"www":{"A":["192.168.1.3"]}, ... }'
}
});
The Search API trap
Our first instinct for looking up records might be to use Stripe's Search API:
const customers = await stripe.customers.search({
query: "metadata['dns_domain']:'${domain}'"
});
This seems fine. However, there's a critical flaw: Stripe's Search API is eventually consistent. The documentation explicitly warns against using it for read-after-write flows. Under normal operating conditions, data becomes searchable in under 1 minute, though propagation delays can occur during outages.
A potential one-minute delay between creating/updating a DNS record and it being resolvable is not great for our DNS server. We need a reliable, immediate read-after-write mechanism.
The List API workaround
The standard list operations on Stripe objects (like stripe.customers.list) are strongly consistent for certain filters. While you can't directly filter on arbitrary metadata with list, you can filter on standard fields like email.
So the workaround: when creating the Customer, we'll store a unique identifier for the domain in the email field. Let's use dns@ followed by the domain name:
const customer = await stripe.customers.create({
name: `DNS records for ${domain}`,
email: `dns@${domain}`,
metadata: { dns_domain: domain, dns_records: '...' }
});
Now we can implement the lookup using stripe.customers.list filtered by email, which gives us immediate consistency.
Limitations (there are many)
- Metadata size limit: the 500-character limit per metadata value is a hard constraint. Complex zones will quickly exceed this.
- Speed: this is slow. Stripe API calls are generally fast, but for DNS servers ultra-fast response times are key.
- No caching: there's no DNS caching implemented server-side. Every query hits the Stripe API.
- Limited record types: only A, CNAME, and MX are implemented here.
- Error handling/testing: this is proof-of-concept code. Proper error handling, input validation, and testing are minimal.
If you even considered running this in production, you are a danger to yourself and others.
What exactly is the point of all of this?
Is this a good idea? No. Is it a viable option if you're in a hurry? Also no. Is it ultimately a practical application of this technology? Absolutely not - it is the jokiest of joke projects.
This project is not production-grade (please don't), but it does show:
- How Stripe metadata can be twisted into storing structured data.
- The limits of flat key-value storage, and how to work around them.
- The importance of understanding API consistency models (Search vs List).
- That "bad ideas" are often the best teaching tools.
The goal here is to show that metadata can be an incredibly powerful and flexible technology - if it can support a functional (albeit terrible) DNS server, then imagine the more practical uses it can bring to your own payment-related workflows.