Skip to main content

ccTLD Standards

From the CTO — Required reading for all product and engineering teams. This is not a suggestion. Every system that manages localized domains, routing rules, or web infrastructure must follow this guide.


Table of Contents

  1. Why This Matters
  2. The Standard: ISO 3166-1 ccTLD
  3. Official Tooling
  4. Database Best Practices
  5. Routing and Infrastructure
  6. Quick Reference

Why This Matters

Domains look like a simple configuration detail. They are not.

A ccTLD is a signal to users, browsers, search engines, and regulators about which country a service belongs to. Using the wrong domain — or an inconsistent one — creates real operational, legal, and security problems.

What Goes Wrong Without a Standard

Geo-routing breaks. When services use generic TLDs (.com, .net) instead of the official country-specific suffix, CDN and DNS-based geo-routing cannot reliably route users to the correct regional edge node. Latency increases. Users in one country may be served infrastructure from another.

SEO is fragmented. Search engines use the ccTLD as one of the strongest signals for regional content targeting. A store operating in Mexico without .mx will rank poorly for Mexican users even if the content is correct. This directly affects organic traffic and revenue.

Compliance requirements are violated. Several countries — including Brazil (.br), Germany (.de), and Russia (.ru) — have data residency or domain registration regulations that tie local operations to a registered ccTLD. Operating without the correct suffix can expose the company to regulatory risk.

Security assumptions fail. Browser security policies, HSTS preloading, and cookie scoping all interact with the domain name. Using inconsistent or unofficial domains creates gaps in CORS policies and cookie security boundaries that are hard to audit.

Data is inconsistent across services. If one service stores mx, another stores .mx, and a third stores MX, joins, lookups, and routing logic all produce different results. Normalization must be enforced at the schema level.

Architectural warning: Domain and routing bugs are configuration-time bugs with user-facing symptoms. They are often discovered in production via support tickets, not monitoring. Enforce the standard at schema validation time, not after deployment.


The Standard: ISO 3166-1 ccTLD

We use ISO 3166-1 alpha-2 country codes as the basis for all ccTLD values. The stored value in any schema or configuration field named cctld must be the official two-letter suffix without a leading dot, in lowercase.

Format Rules

[two-letter ISO 3166-1 alpha-2 code, lowercase, no dot prefix]
ComponentRuleExample
LengthAlways 2 charactersmx, co, es
CaseAlways lowercasemx not MX or Mx
Dot prefixNever stored with a dotmx not .mx
Source standardISO 3166-1 alpha-2See IANA ccTLD list

Reference: Common ccTLDs

CountryccTLD (stored value)Full domain example
Colombiacostore.example.co
Mexicomxstore.example.mx
Spainesstore.example.es
United Statesusstore.example.us
Brazilbrstore.example.com.br
Canadacastore.example.ca
Argentinaarstore.example.ar
United Kingdomukstore.example.co.uk
Germanydestore.example.de
Francefrstore.example.fr

Note on second-level ccTLDs: Some countries use a second-level structure (e.g., .co.uk, .com.br, .com.mx). The cctld field stores only the country code (uk, br, mx). The full domain structure is assembled by the infrastructure layer, not stored in the cctld field.

Normalization Rules

Raw InputStored Value
MXmx
.mxmx
Mxmx
MEX❌ Invalid — reject, do not store
mexico❌ Invalid — reject, do not store
com.mx❌ Invalid — reject, do not store

Rule: Normalize at write time. Accept the ISO 3166-1 alpha-2 code in any case, strip any leading dot, lowercase the result. Reject anything that does not reduce to a valid two-letter code.


Official Tooling

Use the approved libraries for validation. Do not write custom regex or maintain a local copy of the country code list.


TypeScript / JavaScript

Package: i18n-iso-countries

npm install i18n-iso-countries

Why this package:

  • Maintained against the ISO 3166-1 standard with regular updates when countries change.
  • Provides isValid(alpha2) for code validation — no regex needed.
  • Covers all 249 officially assigned alpha-2 codes.
  • Zero runtime dependencies.
  • Tree-shakeable — only import what you need.
import { isValid, alpha2ToAlpha3 } from 'i18n-iso-countries';

function normalizeCctld(raw: string): string {
const normalized = raw.replace(/^\./, '').toLowerCase();
if (!isValid(normalized)) {
throw new Error(`Invalid ccTLD: "${raw}"`);
}
return normalized;
}

Do not write custom regex like /^[a-z]{2}$/. It accepts invalid codes (zz, xx) that are not assigned ccTLDs. Do not maintain a local hardcoded list — it will go stale.


Go

Package: golang.org/x/text/language combined with github.com/biter777/countries

go get github.com/biter777/countries

Why this package:

  • Covers the full ISO 3166-1 alpha-2, alpha-3, and numeric code sets.
  • Actively maintained.
  • Type-safe country lookups with no string parsing pitfalls.
  • No CGo required.
import "github.com/biter777/countries"

func NormalizeCctld(raw string) (string, error) {
normalized := strings.ToLower(strings.TrimPrefix(raw, "."))
c := countries.ByName(normalized)
if c == countries.Unknown {
return "", fmt.Errorf("invalid ccTLD: %q", raw)
}
return normalized, nil
}

Do not use regex-only validation in Go. Do not maintain a local enum of country codes.


Database Best Practices

Column Type

Store cctld values as CHAR(2) in your database. The value is always exactly two characters after normalization.

DecisionRecommendation
TypeCHAR(2)
CaseAlways lowercase — enforce with a CHECK constraint or application layer
NullableOnly if the ccTLD is optional for the entity
DefaultNo default — NULL is better than an empty string or a wrong country
EncodingPlain ASCII — only lowercase letters a–z
-- PostgreSQL: enforce lowercase and length at the column level
ALTER TABLE storefronts
ADD COLUMN cctld CHAR(2) CHECK (cctld = lower(cctld));

Indexes

Always index cctld columns used for filtering or joins. Routing queries by country are high-frequency.

-- Index for single-country lookups
CREATE INDEX idx_storefronts_cctld ON storefronts (cctld);

-- Composite index when filtering by country + another column
CREATE INDEX idx_routes_cctld_active ON routes (cctld, is_active);

Because the value is always a normalized 2-character string, equality lookups (WHERE cctld = 'mx') are fast and fully index-friendly. Mixed-case or dot-prefixed storage destroys this guarantee.

Adding a New Country

When the product expands to a new country:

  1. Validate the ccTLD against the current IANA ccTLD list before any code change.
  2. Add the new value to any application-level allowlist or enum (if one exists).
  3. Do not create a new column per country. The cctld field is the discriminator — use it in WHERE clauses and routing rules.
  4. Update any CDN or DNS routing configuration to cover the new domain before traffic is routed there.

Routing and Infrastructure

Domain Assembly

The cctld field is the source of truth for the country suffix. Assemble full domain names in infrastructure configuration, not in application code.

cctld valueAssembled domainNotes
mxstore.example.com.mxMexico uses second-level structure
costore.example.coColombia is a direct second-level
esstore.example.esSpain is a direct second-level
brstore.example.com.brBrazil uses second-level structure
ukstore.example.co.ukUK uses second-level structure

Rule: Store only the alpha-2 code. Let the CDN or reverse proxy configuration handle second-level structure differences per country. Do not hardcode .com.mx vs .mx logic in application routing tables.

CDN and DNS Configuration

Each ccTLD deployment must have:

  • A registered domain with the national registrar for that country.
  • A valid TLS certificate covering the full domain (wildcard or SAN).
  • HSTS enabled with a max-age of at least 1 year.
  • CDN routing rules that map the ccTLD to the correct regional edge pool.

Do not serve localized content from a generic TLD (.com, .io) with a subdirectory path (/mx/, /co/). This is an acceptable short-term workaround only — it is not a permanent architecture.


Quick Reference

✅ Store as: mx | co | es | br | de
❌ Never store: .mx | MX | Mx | MEX | mexico | com.mx
TaskHow
Accept user or config inputStrip leading ., lowercase, validate against ISO 3166-1
Validate in TypeScriptisValid(normalized) from i18n-iso-countries
Validate in Gocountries.ByName(normalized) != countries.Unknown
Store in DBCHAR(2), enforce lowercase, index the column
Assemble full domainDo it in CDN/infra config, not in application code
Add a new countryValidate ccTLD first, then update routing config before code
LanguageApproved package
Node.js / TypeScripti18n-iso-countries
Gogithub.com/biter777/countries

Questions or edge cases not covered here? Open a discussion in the #engineering-standards channel.