<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Exo Mercado — Writing</title><description>Notes &amp; experiments on building things for the web.</description><link>https://exomercado.dev/</link><item><title>Goodbye PEM files: moving to RDS IAM database authentication</title><link>https://exomercado.dev/blog/goodbye-pem-files/</link><guid isPermaLink="true">https://exomercado.dev/blog/goodbye-pem-files/</guid><description>How I replaced a shared .pem key and a bastion host with AWS RDS IAM auth: short-lived tokens, per-user identity, and nothing left to rotate by hand.</description><pubDate>Mon, 13 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;For years, connecting to our production database looked like this: find the
shared &lt;code&gt;.pem&lt;/code&gt; file (usually in someone&apos;s Slack DMs), SSH into a bastion host,
open a tunnel to the private RDS instance, and paste in a database password
that hadn&apos;t changed since the instance was created. It worked, which is exactly
why nobody fixed it. This is how I finally replaced it with RDS IAM database
authentication, and what I&apos;d tell past me before starting.&lt;/p&gt;
&lt;h2 id=&quot;the-old-setup-and-why-it-aged-badly&quot;&gt;The old setup, and why it aged badly&lt;/h2&gt;
&lt;p&gt;The database lived in a private subnet, as it should. The only way in was an
SSH tunnel through a bastion:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh -i team-bastion.pem -N -L 5432:mydb.abc123.ap-southeast-1.rds.amazonaws.com:5432 ec2-user@bastion.example.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Humans were only half of it. Our Go services run on Lambda, and the functions
needed the same path in, so the key was baked straight into the deployment
artifact and the tunnel was opened in code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;//go:embed team-bastion.pem
var bastionKey []byte

func connect(ctx context.Context) (*pgx.Conn, error) {
	signer, err := ssh.ParsePrivateKey(bastionKey)
	if err != nil {
		return nil, err
	}

	bastion, err := ssh.Dial(&quot;tcp&quot;, &quot;bastion.example.com:22&quot;, &amp;amp;ssh.ClientConfig{
		User:            &quot;ec2-user&quot;,
		Auth:            []ssh.AuthMethod{ssh.PublicKeys(signer)},
		HostKeyCallback: ssh.InsecureIgnoreHostKey(), // yes, really
	})
	if err != nil {
		return nil, err
	}

	cfg, err := pgx.ParseConfig(os.Getenv(&quot;DATABASE_URL&quot;)) // static password inside
	if err != nil {
		return nil, err
	}
	cfg.DialFunc = func(ctx context.Context, network, addr string) (net.Conn, error) {
		return bastion.Dial(network, addr)
	}
	return pgx.ConnectConfig(ctx, cfg)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every invocation paid for an SSH handshake before it could run a query, and
every function in the account carried a copy of the key to production.&lt;/p&gt;
&lt;p&gt;The problems weren&apos;t hypothetical. We hit all of them:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;One key, many people.&lt;/strong&gt; The &lt;code&gt;.pem&lt;/code&gt; file was shared, so rotating it meant
coordinating with everyone at once. So we didn&apos;t rotate it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The key shipped with every build.&lt;/strong&gt; Because the Lambdas embedded it,
rotating the key also meant rebuilding and redeploying every function that
carried a copy. One secret, dozens of deployment artifacts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Offboarding was a lie.&lt;/strong&gt; When someone left, they kept a working copy of the
key unless we rotated it (see above).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No audit trail.&lt;/strong&gt; Every connection came from &lt;code&gt;ec2-user&lt;/code&gt; through the same
tunnel. CloudTrail and the database logs both said the same thing: someone
connected. Great, thanks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The bastion itself.&lt;/strong&gt; One more EC2 instance to patch, monitor, and pay
for, whose entire job was being a hole in the network.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Static database credentials.&lt;/strong&gt; The app password and the human password
were both long-lived strings sitting in password managers and, let&apos;s be
honest, a few shell histories.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;how-rds-iam-auth-works&quot;&gt;How RDS IAM auth works&lt;/h2&gt;
&lt;p&gt;The idea is simple: instead of a password, the database accepts a short-lived
token signed by AWS. Your IAM identity becomes your database identity. There
are four pieces.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Enable IAM auth on the instance.&lt;/strong&gt; A checkbox in the console, or:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;aws rds modify-db-instance \
  --db-instance-identifier mydb \
  --enable-iam-database-authentication \
  --apply-immediately
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. Create a database user mapped to IAM.&lt;/strong&gt; For Postgres, you grant the
&lt;code&gt;rds_iam&lt;/code&gt; role. For MySQL you&apos;d create the user with
&lt;code&gt;AWSAuthenticationPlugin&lt;/code&gt; instead.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE USER exo_mercado WITH LOGIN;
GRANT rds_iam TO exo_mercado;
GRANT readonly TO exo_mercado; -- normal grants still apply
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3. Attach an IAM policy&lt;/strong&gt; allowing &lt;code&gt;rds-db:connect&lt;/code&gt; for that user. Note the
resource ARN uses the DbiResourceId (starts with &lt;code&gt;db-&lt;/code&gt;), not the instance
name:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [
    {
      &quot;Effect&quot;: &quot;Allow&quot;,
      &quot;Action&quot;: &quot;rds-db:connect&quot;,
      &quot;Resource&quot;: &quot;arn:aws:rds-db:ap-southeast-1:123456789012:dbuser:db-ABCDEFGHIJKL01234/exo_mercado&quot;
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;4. Generate a token and use it as the password.&lt;/strong&gt; The token is a presigned
request, valid for 15 minutes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;TOKEN=$(aws rds generate-db-auth-token \
  --hostname mydb.abc123.ap-southeast-1.rds.amazonaws.com \
  --port 5432 \
  --username exo_mercado)

PGPASSWORD=$TOKEN psql \
  &quot;host=mydb.abc123.ap-southeast-1.rds.amazonaws.com port=5432 dbname=app user=exo_mercado sslmode=verify-full sslrootcert=global-bundle.pem&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Yes, there&apos;s still a &lt;code&gt;.pem&lt;/code&gt; file in that command. It&apos;s the public RDS CA
bundle, not a secret, and I find that funny.&lt;/p&gt;
&lt;h2 id=&quot;the-go-side-lambda-through-rds-proxy&quot;&gt;The Go side: Lambda through RDS Proxy&lt;/h2&gt;
&lt;p&gt;For the Lambdas we didn&apos;t point pgx at the instance directly. Lambda churns
connections by design, so we put RDS Proxy in front and enabled IAM auth on
the proxy. The proxy holds the warm pool and speaks to the database using
credentials it keeps in Secrets Manager; our code only ever authenticates to
the proxy with a token. Two details matter here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;rds-db:connect&lt;/code&gt; policy for a proxy uses the proxy id (starts with
&lt;code&gt;prx-&lt;/code&gt;) in the resource ARN, not the instance&apos;s &lt;code&gt;db-&lt;/code&gt; id, and it goes on the
Lambda&apos;s execution role. The function itself holds no credentials at all.&lt;/li&gt;
&lt;li&gt;Tokens expire in 15 minutes, so you generate one per new connection, not one
per deploy. pgx has the perfect hook for this: &lt;code&gt;BeforeConnect&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;const proxyEndpoint = &quot;app-proxy.proxy-abc123.ap-southeast-1.rds.amazonaws.com:5432&quot;

func newPool(ctx context.Context) (*pgxpool.Pool, error) {
	awsCfg, err := config.LoadDefaultConfig(ctx)
	if err != nil {
		return nil, err
	}

	cfg, err := pgxpool.ParseConfig(
		&quot;postgres://app_service@&quot; + proxyEndpoint + &quot;/app?sslmode=verify-full&quot;)
	if err != nil {
		return nil, err
	}
	cfg.BeforeConnect = func(ctx context.Context, cc *pgx.ConnConfig) error {
		token, err := auth.BuildAuthToken(
			ctx, proxyEndpoint, awsCfg.Region, cc.User, awsCfg.Credentials)
		if err != nil {
			return err
		}
		cc.Password = token
		return nil
	}
	return pgxpool.NewWithConfig(ctx, cfg)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;auth.BuildAuthToken&lt;/code&gt; comes from
&lt;code&gt;github.com/aws/aws-sdk-go-v2/feature/rds/auth&lt;/code&gt; and does the same thing as
&lt;code&gt;generate-db-auth-token&lt;/code&gt;, using whatever credentials the Lambda role provides.
Every fresh connection in the pool gets a fresh token, so expiry never bites.
Compare this to the SSH version above: no embedded key, no tunnel, no static
password, and about half the code.&lt;/p&gt;
&lt;p&gt;One pleasant surprise: the proxy endpoint presents a certificate that public
roots already trust, so &lt;code&gt;verify-full&lt;/code&gt; works without shipping the RDS CA
bundle in the artifact.&lt;/p&gt;
&lt;h2 id=&quot;the-gotchas&quot;&gt;The gotchas&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Tokens expire in 15 minutes.&lt;/strong&gt; That&apos;s the point, but it means anything that
reconnects needs to generate a fresh token each time, not cache one in an env
var. In Go, &lt;code&gt;BeforeConnect&lt;/code&gt; handles it (see above). For humans, a small
wrapper script around &lt;code&gt;psql&lt;/code&gt; does the same job.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SSL is required.&lt;/strong&gt; Connections without TLS are rejected outright. Set
&lt;code&gt;sslmode=verify-full&lt;/code&gt; and ship the CA bundle; don&apos;t settle for &lt;code&gt;require&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Auth rate limits are real.&lt;/strong&gt; IAM auth adds work per connection, and AWS
documents a connection-rate ceiling. For humans running &lt;code&gt;psql&lt;/code&gt; this is
irrelevant. For Lambda it absolutely is not, which is the other reason the
functions go through RDS Proxy: the database sees a stable pool instead of a
stampede of token validations. If a proxy is overkill for your case, a pooler
like PgBouncer covers the same ground.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;You still need a network path.&lt;/strong&gt; IAM auth replaces the password, not the
VPC. The database is still in a private subnet, so you need to reach it
somehow: client VPN, SSM port forwarding to any instance in the VPC, or just
running from workloads already inside the VPC. We went with SSM port
forwarding for humans, which killed the bastion too, since SSM needs no open
inbound ports and no SSH keys at all.&lt;/p&gt;
&lt;h2 id=&quot;what-actually-improved&quot;&gt;What actually improved&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No shared secret.&lt;/strong&gt; There&apos;s no PEM file for the database and no password
to pass around. Access is your IAM identity, full stop.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Per-user audit trail.&lt;/strong&gt; &lt;code&gt;exo_mercado&lt;/code&gt; connects as &lt;code&gt;exo_mercado&lt;/code&gt;. Database
logs and CloudTrail finally agree on who did what.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Credentials expire on their own.&lt;/strong&gt; A leaked token is worthless in 15
minutes. Rotation stopped being a project and became a property.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Offboarding is one action.&lt;/strong&gt; Disable the IAM user and every database they
could reach is closed to them, immediately, without touching the database.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deploys stopped carrying secrets.&lt;/strong&gt; The Lambda artifact is code and
nothing else. Rotating anything no longer touches a build pipeline.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One less server.&lt;/strong&gt; The bastion is gone, along with its patch schedule.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The migration itself took an afternoon per database, most of it spent standing
up the proxy, swapping the tunnel code for &lt;code&gt;BeforeConnect&lt;/code&gt;, and updating
runbooks. The hardest part was believing it
would be that small. If you&apos;re still passing a &lt;code&gt;.pem&lt;/code&gt; file around to reach
RDS, this is one of the rare security upgrades that makes daily life easier
instead of worse.&lt;/p&gt;
</content:encoded><category>aws</category><category>rds</category><category>iam</category><category>security</category><category>postgres</category><category>go</category><category>lambda</category></item><item><title>Building RBAC from scratch</title><link>https://exomercado.dev/blog/rbac-from-scratch/</link><guid isPermaLink="true">https://exomercado.dev/blog/rbac-from-scratch/</guid><description>Why I built a role-based access control module by hand for an enterprise platform, the model I landed on, and the gotchas that only show up in production.</description><pubDate>Tue, 30 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A few years ago I built the access-control layer for an internal enterprise
platform from the ground up — no off-the-shelf authorization library, just Go,
DynamoDB, and a model we could actually reason about. This is what I learned.&lt;/p&gt;
&lt;p&gt;The platform served at least 500 users, and access control had to stay
flexible: roles assigned to users on the fly, with the permissions on each
role adjustable per module.&lt;/p&gt;
&lt;h2 id=&quot;why-build-it-instead-of-reaching-for-a-library&quot;&gt;Why build it instead of reaching for a library&lt;/h2&gt;
&lt;p&gt;There are good authorization libraries out there, and for most projects I&apos;d use
one. We didn&apos;t, for two reasons.&lt;/p&gt;
&lt;p&gt;First, our permissions were tied to systems we were integrating — Azure AD for
identity, plus JIRA and Bitbucket for the actual work. A generic library would
have meant bending our domain to fit its model anyway, so the &quot;free&quot; part wasn&apos;t
really free.&lt;/p&gt;
&lt;p&gt;Second, authorization is the one place I want zero magic. When someone asks
&quot;why can this user do that?&quot;, I need to answer it by reading code, not by
tracing through a framework&apos;s policy engine. Building it ourselves kept the
whole decision path in plain sight.&lt;/p&gt;
&lt;h2 id=&quot;the-model&quot;&gt;The model&lt;/h2&gt;
&lt;p&gt;I kept it deliberately boring. Three nouns:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Permissions&lt;/strong&gt; are the atoms — a verb on a resource, like &lt;code&gt;repo:read&lt;/code&gt; or
&lt;code&gt;user:invite&lt;/code&gt;. Nothing checks a role directly; everything checks a permission.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Roles&lt;/strong&gt; are named bundles of permissions (&lt;code&gt;admin&lt;/code&gt;, &lt;code&gt;maintainer&lt;/code&gt;, &lt;code&gt;viewer&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Assignments&lt;/strong&gt; bind a user to a role, optionally scoped to a team or project.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The single most important decision was that &lt;strong&gt;code only ever checks
permissions, never roles&lt;/strong&gt;. Roles are a convenience for humans assigning access;
the enforcement layer doesn&apos;t know they exist. That one rule is what kept the
system from rotting: when product asked for &quot;let maintainers do X too,&quot; it was a
one-line change to a role&apos;s permission set, not a hunt through the codebase for
every &lt;code&gt;if role == &quot;maintainer&quot;&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;storing-it-in-dynamodb&quot;&gt;Storing it in DynamoDB&lt;/h2&gt;
&lt;p&gt;DynamoDB rewards you for knowing your access patterns up front, so I started
there. The hot path is always the same question: &lt;em&gt;given this user, what can
they do?&lt;/em&gt; So role assignments are keyed by user, and resolving a user&apos;s
effective permissions is a single query plus a couple of cheap role lookups that
are easy to cache.&lt;/p&gt;
&lt;p&gt;The trap I almost fell into was modelling permissions as something you compute
with a scan. You never want authorization to depend on a table scan — it&apos;s the
one query that runs on every request, and it has to stay fast and predictable.
Keep the &quot;what can this user do&quot; lookup down to a key-based read and you&apos;re fine.&lt;/p&gt;
&lt;h2 id=&quot;enforcing-it-in-go&quot;&gt;Enforcing it in Go&lt;/h2&gt;
&lt;p&gt;Enforcement lived in one place: middleware. The handler declares the permission
it needs, and the middleware resolves the caller&apos;s effective permissions and
either continues or returns a 403.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func RequirePermission(p Permission) Middleware {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            user := UserFromContext(r.Context())
            if !user.Permissions.Has(p) {
                http.Error(w, &quot;forbidden&quot;, http.StatusForbidden)
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The win here isn&apos;t the code — it&apos;s that there&apos;s exactly &lt;em&gt;one&lt;/em&gt; code path that
grants or denies. No handler does its own ad-hoc check. If authorization has a
bug, I know the one file to open.&lt;/p&gt;
&lt;h2 id=&quot;the-gotchas-nobody-warns-you-about&quot;&gt;The gotchas nobody warns you about&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Caching effective permissions is a footgun.&lt;/strong&gt; You&apos;ll want to cache the
resolved permission set because it&apos;s read constantly — but the moment you do,
revoking access stops being instant. I cached with a short TTL and an explicit
invalidation when a user&apos;s assignments change. Decide your staleness budget
on purpose; don&apos;t let it be an accident.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The frontend hides, the backend enforces.&lt;/strong&gt; It&apos;s tempting to treat a hidden
button as security. It isn&apos;t. The UI consumes the same permission set purely to
decide what to &lt;em&gt;show&lt;/em&gt; — every real decision still happens on the server. Getting
this boundary right is most of the battle.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Roles multiply if you let them.&lt;/strong&gt; The first time a request doesn&apos;t fit an
existing role, the lazy fix is a new role. Do that enough and you&apos;ve got forty
near-identical roles nobody understands. Because we checked permissions and not
roles, we could usually adjust an existing role instead of minting a new one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Deny-by-default, always.&lt;/strong&gt; The absence of a permission is a &quot;no.&quot; New
resources and new endpoints start locked, and access is something you add
deliberately. The inverse — open by default, lock things down later — is how you
end up with a breach.&lt;/p&gt;
&lt;p&gt;{/* TODO(exo): drop in a real story here — a specific bug, a tricky permission
    edge case, or a moment the deny-by-default rule saved you. This is the part
    readers remember. */}&lt;/p&gt;
&lt;h2 id=&quot;would-i-do-it-again&quot;&gt;Would I do it again?&lt;/h2&gt;
&lt;p&gt;For that platform, yes. The model was small enough to hold in my head, the
enforcement path was a single file, and &quot;why can this user do that?&quot; was always
answerable. If your authorization rules are genuinely complex — hierarchies,
attribute-based conditions, relationship graphs — reach for something purpose-
built instead. But for plain role-based access, hand-rolling it bought us
clarity that was worth far more than the few days it took to write.&lt;/p&gt;
&lt;p&gt;The whole thing comes down to one habit: &lt;strong&gt;check permissions, not roles, and
deny by default.&lt;/strong&gt; Get that right and the rest is just plumbing.&lt;/p&gt;
</content:encoded><category>go</category><category>backend</category><category>architecture</category><category>rbac</category></item><item><title>Colophon — how this site is built</title><link>https://exomercado.dev/blog/colophon/</link><guid isPermaLink="true">https://exomercado.dev/blog/colophon/</guid><description>A tour of the Signal Path design behind this site: the Copper &amp; Carbon palette, the Canvas service-mesh hero, and the small details underneath.</description><pubDate>Mon, 29 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I like reading the colophon on other people&apos;s sites, so here&apos;s mine. The design
has a name, Signal Path, and the idea behind it is a page that behaves like a
circuit diagram: signals routed along clean paths, nothing on screen that isn&apos;t
also doing a job.&lt;/p&gt;
&lt;h2 id=&quot;the-stack&quot;&gt;The stack&lt;/h2&gt;
&lt;p&gt;Still static &lt;a href=&quot;https://astro.build&quot;&gt;Astro&lt;/a&gt;, still zero JavaScript shipped by
default. Pages are plain &lt;code&gt;.astro&lt;/code&gt;; this writing lives in &lt;strong&gt;MDX&lt;/strong&gt; through typed
content collections, so a post with broken frontmatter fails the build instead
of shipping.&lt;/p&gt;
&lt;h2 id=&quot;color-and-type&quot;&gt;Color and type&lt;/h2&gt;
&lt;p&gt;Styling is &lt;strong&gt;Tailwind CSS v4&lt;/strong&gt;, configured CSS-first. There&apos;s no config file;
the tokens live in an &lt;code&gt;@theme&lt;/code&gt; block in &lt;code&gt;global.css&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The palette is called &lt;strong&gt;Copper &amp;amp; Carbon&lt;/strong&gt;. Carbon (&lt;code&gt;#0C1014&lt;/code&gt;) is the ground.
Copper (&lt;code&gt;#DE8B52&lt;/code&gt;) is the one accent that means &quot;active&quot; or &quot;interactive,&quot; and
verdigris (&lt;code&gt;#5FB8A3&lt;/code&gt;) is the second signal, reserved for &quot;steady&quot; status.
Dark is the canonical mode. There&apos;s a light &quot;datasheet&quot; mode too, but the whole
site was designed dark-first, and the light theme reads like a printed spec
sheet.&lt;/p&gt;
&lt;p&gt;Type does real work here. &lt;strong&gt;Archivo&lt;/strong&gt; (variable) is the display face, and its
width axis actually animates. &lt;strong&gt;IBM Plex Sans&lt;/strong&gt; carries the body copy.
&lt;strong&gt;JetBrains Mono&lt;/strong&gt; is the machine voice: labels, timestamps, anything the site
says in its own robotic register.&lt;/p&gt;
&lt;h2 id=&quot;the-hero&quot;&gt;The hero&lt;/h2&gt;
&lt;p&gt;The hero is a &lt;strong&gt;Canvas 2D service mesh&lt;/strong&gt;. Nodes and packets drift around, pulled
toward your cursor like it has a bit of gravity. As you scroll, the mesh
collapses along a line I call the Seam, which folds down into the fixed rail on
the left. A scattered network becoming one clean path is the whole Signal Path
idea in a single gesture.&lt;/p&gt;
&lt;h2 id=&quot;the-name&quot;&gt;The name&lt;/h2&gt;
&lt;p&gt;My name doesn&apos;t just fade in, it decodes. The glyphs flicker through random
characters, then settle, while Archivo&apos;s width axis widens them into place.
There&apos;s a subtle shimmer over the top, drawn in WebGL with OGL, just enough to
catch the light.&lt;/p&gt;
&lt;h2 id=&quot;the-command-palette&quot;&gt;The command palette&lt;/h2&gt;
&lt;p&gt;Hit &lt;kbd&gt;⌘K&lt;/kbd&gt; (or &lt;kbd&gt;Ctrl&lt;/kbd&gt;+&lt;kbd&gt;K&lt;/kbd&gt;) for a terminal-style command
palette: jump around, flip the theme, copy my email, find the easter egg. It&apos;s
the fastest way through the site if you&apos;d rather keep your hands on the keyboard.&lt;/p&gt;
&lt;p&gt;Moving between pages uses Astro&apos;s &lt;strong&gt;View Transitions&lt;/strong&gt;, so navigation animates
instead of blinking white.&lt;/p&gt;
&lt;h2 id=&quot;the-parts-you-dont-see&quot;&gt;The parts you don&apos;t see&lt;/h2&gt;
&lt;p&gt;Every animation checks &lt;code&gt;prefers-reduced-motion&lt;/code&gt; first. If you&apos;ve asked your OS
to calm things down, or JavaScript never runs, you still get the whole page,
finished, with nothing hidden. And the color tokens run through a build-time
contrast check: if any text-and-background pair drops below WCAG AA, the build
fails. Accessibility that only holds when someone remembers to check it isn&apos;t
accessibility.&lt;/p&gt;
&lt;p&gt;That&apos;s the tour. Thanks for reading the colophon.&lt;/p&gt;
</content:encoded><category>meta</category><category>astro</category><category>design</category></item></channel></rss>