Highport orbital control

Versions, edits and the long life of a tile

Every version you have ever published is still on the board, and still docked to somebody's site. The permanence is deliberate, and it is why anybody trusts a tile at all.

Editing a tile means writing a new record at the same record key. The AT-URI stays the same, the record CID changes, and we index the new version alongside every version before it. What does not happen is the part that surprises people: no site moves. A site binds a tile by CID, so every publisher already using your tile keeps serving the exact bytes they pinned, and keeps serving them until they decide otherwise. The mechanism is on Tiles. This page is what it costs you and what to do about it.

A new version moves nothing

Two things follow, and both are worth knowing before your tile has users.

You cannot ship a fix to your existing users. There is no push. A site bound to version 1 serves version 1 forever, whatever you publish afterwards, whatever is wrong with it. Every upgrade is a decision made by the site's owner, in their own record, on their own schedule.

You cannot withdraw a bad version from the sites already using it. Publishing a replacement does not retire it. Deleting the whole tile does not retire it. There is no per-version recall, and nothing you own reaches into somebody else's site.

Both of those are the guarantee, not a limitation. A site owner binds a design from a stranger precisely because a stranger cannot change what their page serves, and cannot take it away. You are the stranger. The way to work inside it is to treat a published version as published: test before you write, and assume anything that ships is load-bearing for somebody you will never meet.

Publishing a new version

The one rule that ruins a tile if you get it wrong: a new version is putRecord at the same record key. createRecord mints a new key, which gives you a second tile rather than a new version of the first.

Thing On a new version
Record key Unchanged. A TID, minted once when you first publish
AT-URI Unchanged: at://{did}/space.highport.sites.tile/{rkey}
Record CID New, because the bytes changed. This is what a site pins
Earlier versions Kept, indexed, and still bindable. A site can pin any of them, not only the newest

In the Hub, https://hub.highport.space/tiles lists your tiles and Edit opens the builder at /tiles/{rkey}/edit. It loads the live record from your account, not from our index, so what you edit is what you last published. While that is in flight the page reads Loading the current version…, and if the record cannot be read it says Could not read the current tile. The publish button reads Publish new version, and the page's own lede states the promise: Publishing from here creates a new version. Sites already bound to this tile keep serving the version they pinned — bindings are by CID.

Everything else is the flow described in Building a tile and Validate before you write, with the same validation and the same submit-exactly-what-validate-returned rule, and with the record key supplied instead of minted. From your own tooling, the same applies: keep the rkey and reuse it. The publish-tile example reads it from BARD_TILE_RKEY and says so in its own warning.

Two tabs editing the same tile is last-write-wins at your account, and the person who lost sees nothing. Nothing downstream is damaged by it: both writes become versions, both become rows, and every bound site keeps serving what it pinned. What is lost is one edit of yours, not anybody's site.

Every version you have ever published is still on file. Somebody is still using the third one.

Saying what changed

There is no changelog field on a tile record. name and description are the only prose you get, and description, up to 300 graphemes, is what the directory shows beside your tile. It describes the newest published version, so rewriting it changes what a new site owner reads and changes nothing for anyone already bound.

The version history exists so a site owner can see where they stand. A tile's page at https://hub.highport.space/explore/tiles/{did}/{rkey} folds it into a Technical details disclosure instead of a section of its own, because none of it helps somebody decide whether they want the tile:

Row What it is
This version The CID of the version being viewed
Newest published The newest indexed version at this URI. Shown only when it differs from the one being viewed
Indexed When we finished indexing this version
Contents Entry count and total size

Under it, in prose: A site binds one version and never follows a later one. Publishing an update leaves every site already using this tile exactly as it was.

The offer to move is made in the site owner's editor, not in yours. When the version they bound is not the newest, their site editor shows: The author has published a newer version. Nothing changes on your live page until you publish, and anything the new version needs from you appears below, with a Use the newer version button. Pressing it only changes the CID in their draft; nothing on their live page changes until they publish. That flow is theirs, and it is described in Using a tile on your site.

Two practical consequences for how you version:

Reading a specific version programmatically: space.highport.sites.getTile with no cid answers the newest version at that URI, whatever state it is in, and with a cid answers that exact one. See XRPC.

Pinning an older version

Everything above is what a new version does to you. This is what an older one is still good for, and it is the site owner's move and not yours. Every version you have published stays bindable, and a site can go backwards as readily as forwards. Publishing a new version does not touch the older rows, because nothing marks a version superseded. So a version that indexed ready stays ready, keeps its files, and is accepted at validation for as long as it exists.

Nothing in the interface will do it. The picker lists one row per tile at its newest version and binds that CID, the tile's page in the directory offers no per-version action, and the only version control in a site editor is Use the newer version. So pinning an older version means writing the site record by hand: set source.cid to that version's CID, hand the record to space.highport.manage.validate, and write back what it returned with com.atproto.repo.putRecord at the hostname key. Validate before you write is that sequence in full.

Once the older CID is in the record, the Hub keeps it. A site editor reads the binding out of the record and not out of our index, so editing the details and republishing from the Hub leaves the version where it is. The newer-version paragraph and its button appear as they would for anyone, and nothing presses them.

Nothing lists a tile's versions. There is no listTileVersions method and no version history anywhere in the Hub. A tile's page shows the version it is displaying, and the newest one when that differs. Nothing more. So the CID has to come from somewhere you already have it:

Where What it gives you
A tile's page in the directory, under Technical details The CID of the version on screen. Worth writing down before a newer one exists
space.highport.sites.getSite?domain= tile.cid for any site still bound to the version you want, and tile.latestCid when a newer one has been published
/_bard/site.json on that same site source.tile.cid, the same fact, served by the site itself
space.highport.sites.getTile?uri=&cid= Whether we still hold that exact version, and what state it is in

Naming a version we do not hold is refused at validation as TileVersionUnavailable, and the message names the newest one we do hold. An older version that landed incomplete is refused as TileInvalid. It was never bindable, and its age is not what is wrong with it.

Withdrawing a version

No. Not one version, and not by its author.

There is exactly one write in the whole system that changes the state of a single version, and it belongs to the delete path, which, as the delete section below explains, does not fire on a delete from your own repository. Everything else is per URI. An operator suspending a tile names the URI and means every version of it, because "this tile is not to be bound" is a statement about the tile and not about one of its versions. There is no author-facing method that takes one version out of circulation.

And past all of it: even deleting the record does not stop the version resolving for a site that already bound it. It is the design, not a gap in it.

Deleting the tile

Deleting a tile is com.atproto.repo.deleteRecord against your own account. The record is yours, so removing it is a write to your own repository, and what changes is what that repository holds.

There are two delete controls in the Hub and they behave differently. The tile's own page in the directory shows a Danger zone to the author only, and its dialog requires you to type the tile's display name before the button is enabled. The tiles list at https://hub.highport.space/tiles has a Delete button on each row that acts immediately, with no confirmation dialog. Use the first one if you want the chance to change your mind.

What After the delete
The record in your account Gone. The editor can no longer load it and reports Could not read the current tile
Your own tiles list Gone from it. /tiles reads your account directly, not our index
Sites already bound Untouched at every layer. No snapshot, no pointer, no byte is changed. They keep serving
The bytes Kept indefinitely while any active or retained snapshot references them
The record key Still yours. Writing to the same key with putRecord creates the tile again, and republishing a key we already hold is exempt from the tile quota

What a delete does not currently do

A delete event carries no CID, meaning no identifier for a version, and every state we hold about a tile is per version. So a delete from your own repository marks nothing in our index today, and four things you would reasonably expect do not happen:

The Hub's own delete copy, It stops appearing in the directory and nobody can bind it again, describes what a delete is defined to do and not what it does today. Take it as a plan, and delete a tile because you want it out of your own repository, not because you need it out of the directory. There is no author-facing way to force the second; removing a tile from the directory is an operator action.

One thing is unaffected either way, and it was never a promise to begin with: if a version of your work is being served somewhere and you need it taken down, deletion is not the mechanism and no author-facing mechanism is. That is an operator action against the site, and it is theirs to take.

Incomplete tiles

A tile lands incomplete when we could not fetch every file the record names, because the blobs are no longer held by your account. It is not rejected, because "this tile does not work" and "this tile does not exist" are different answers and the second one would be wrong.

An incomplete version:

In the Hub, /tiles shows a page-level callout reading N tiles are incomplete. Highport could not fetch every blob. The one remedy is to re-upload the missing files and publish a new version, and the row reads Missing files — not safe to use.

Only you can fix it, and only by publishing a new version. Operators deliberately cannot: space.highport.admin.setTileState moves versions between ready and suspended and leaves incomplete alone in both directions, instead of restoring a version that still cannot be served whole. space.highport.manage.reindex re-reads the record and re-runs the pipeline, and helps only if the files are back in your account. An account is free to collect a blob no record references, and after that no retry produces it. Re-upload, then publish.

Fix it promptly, not at leisure. If the newest version at your URI is the incomplete one, site owners bound to an earlier version are still offered the upgrade, and the publish is refused when they take it.

The icon case

A tile's icon is a field beside the manifest, not an entry inside it, and it is fetched alongside the files a tile needs rather than among them. That is deliberate, so a missing icon cannot make a tile unbindable. The cost of that choice is that an icon failure is quiet: the tile indexes ready, and space.highport.sites.getTileIcon answers 404 for it, which shows up as a broken image in the directory.

Tiles indexed before icons were fetched at all keep the broken icon. Nothing re-runs a finished job, so the fix is a republish or a space.highport.manage.reindex call for that tile URI.

Suspension and takedown

An operator can suspend a tile with space.highport.admin.setTileState. It names the tile by URI and moves every ready version to suspended; setting it back to active is the exact inverse and touches nothing else.

Thing While suspended
New bindings Refused. Validation answers TileSuspended (HTTP 403), and the indexer refuses the same way after a commit
The directory The tile stops being listed
Sites already bound Keep serving. Suspension writes a state and invalidates nothing; taking down a site that serves a suspended tile is a separate action against that site
incomplete and deleted versions Left alone, in both directions

The suspension moves the versions that exist when the operator sets it. A version published afterwards is admitted on its own terms and lands ready, so a suspension is not a bar on the record. The mechanism that stops new versions is the denylist, and it is also an operator's.

If your account itself becomes inactive, whether suspended, taken down, deactivated or deleted at your provider, every ready version you have published is marked suspended too, and the sites bound to them keep serving. When the account is active again, those versions are restored, and an operator's separate suspension of one of them is not lifted along with it.

The tile quota

BARD_MAX_TILES_PER_DID is 20 by default, and it counts distinct record keys, not versions. So a tile you have republished forty times is one tile against the quota. Tiles you have prepared but not yet published do count, because preparing one makes us fetch and store its files.

Republishing a key we already hold is exempt from the check, so sitting exactly at the limit never stops you publishing a new version of a tile you already have. It does stop you starting a new one, and note from the delete section above that deleting a tile does not currently give the slot back.

The two names this limit is refused under, and every size limit a tile is subject to, are in Limits and quotas. What a parameter declaration may contain is in Parameters.

Tiles · 4 of 4