Highport orbital control
Spaces
Private records, public website. A space keeps the paperwork off the open channel and changes nothing about who may read the finished pages.
An atproto space is a permissioned store on the server that holds your account: a second, private place to keep records, off the public stream, readable only by the people you admit and the applications you allow. Highport reads site and tile records out of it and publishes them exactly the way it publishes records from your public account. What gets served does not change at all.
Your account's server, which the Hub calls your PDS, has to support spaces. Highport works that out at sign-in, from what your server actually granted, without asking it anything. If yours does not, the Spaces area stays in the menu and explains itself instead of disappearing: Your PDS does not support spaces yet, so the Spaces area is hidden. The space: scopes were not granted for this session, which is what a server without permissioned repositories answers. Nothing else about your account is affected.
What a space is for
Work you are not ready to announce. A site for a small group. A tile you want a few people to look over before anybody else can find it.
The private part is narrower than it sounds. The records stay out of the public stream, and a tile published into a space never appears in the public tile directory. The website is a public website, because a domain serving bytes to the internet is public by definition. A space keeps the record quiet, not the site: a site published from a space is still listed in the public site directory, carrying the space it came from.
If what you want is to restrict who may read the pages, that is a different mechanism with different machinery behind it. See asking visitors to sign in.
Only the authority publishes
A space has an authority, the account it is anchored on, which is yours if you made it. It also has members, the identities the authority admits. Highport reads only the authority's records. Anything another member writes into your space belongs to them, and is never indexed and never served.
So a space is a private extension of one person's publishing, not a shared workspace. Members read, and their reading is what keeps Highport's access alive. A member opening your space page is told so once: Only this space's owner publishes here. You can follow it, and opening it keeps Highport's access alive for everybody.
Highport keeps no member list of its own. The Members tab is your server's answer, read live, every time.
How Highport gets in
Highport is a delegate. It holds no identity that could be a member of your space, so it cannot admit itself. Every hour of access it has was handed over by somebody who can already read the space, and it lapses on its own.
Orbital Control cannot sign itself in. Somebody already inside does that, a few hours at a time, for as long as anybody keeps visiting.
The exchange is three steps, and the last two happen inside one request:
- You, or any member, open the space in the Hub.
- Highport, using your session, asks the server that holds your account for a delegation token. It is addressed to that one space, good for sixty seconds, spendable once, and it names no client application. It never reaches your browser.
- Highport presents that token at the space's own host and gets back a space credential, bound to a key Highport generated for this space and uses for nothing else.
Before storing that credential, Highport checks two things and refuses on either, rather than warning: that the authority's own key signed it, and that it is bound to the key Highport actually holds. A credential bound to somebody else's key would be refused on every read for the length of its term, and that failure is indistinguishable from a host being down.
Your sign-in is what makes step 2 possible, and it asks for space access as part of one permission set. See what that grant covers. The credential's term is your server's to set, and two hours is the usual answer.
Keeping the door open
The credential expires, and nothing renews it on a timer. Four things renew it, and every one of them is somebody using the space:
| What renews access | Who can do it |
|---|---|
| Opening the Spaces list | the authority, since the list only ever shows your own spaces |
| Opening a space's own page | anybody who can read the space |
| Pressing Renew access on the space page | anybody who can read the space |
| Saving the Settings tab | the authority |
A renewal only fires when less than thirty minutes are left on the credential, so an ordinary visit costs nothing. Any member can renew. That is the point of the design, and it is why a space whose authority publishes from some other client stays readable as long as somebody looks at it.
Highport recognizes a member for thirty days after their last successful grant. A member who has not opened the space in that time is treated as a stranger. The link answers Could not load this space, which is the same card a mistyped key or an unknown space produces, and nothing in the Hub offers them a way back, because the only control that renews is on the page they can no longer load. The authority is never affected by this.
When access lapses
A space nobody opens eventually holds no live credential, and Highport cannot read it again until somebody grants.
The sites it already published keep serving. Nothing expires, nothing is demoted, no snapshot is aged out. The bytes at the domain are exactly what the last successful sync produced, and they stay that way for as long as the deployment runs.
What stops is updating. Records you write into the space while the credential is gone are not lost either. Your server's notifications are still received and queued, and the next renewal drains all of them in one pull. The banner counts them for you: 3 changes are waiting.
| Banner on the space page | What has happened | What it offers |
|---|---|---|
| Highport's access to this space has expired | No live credential | Opening the page renews it; Sync now when changes are waiting |
| Highport stopped being notified about this space | The notification registration lapsed | Sync now, which re-registers it and catches up anything missed |
| Highport could not reach this space's host | The host did not complete the exchange | Sync now, though it keeps trying on its own |
| This space's allow list does not include Highport | See below | The line to add, and no button. The fix is on your server |
Sync now is the authority's button. Renew access is everybody's.
None of those four states has anything to do with whether your site is up, and nothing on the space page says so either way. A red access banner above a Sites tab listing active domains is the honest picture of a lapsed space, not a contradiction.
Allow lists
A space can restrict which applications may read it. If yours does, Highport's client identifier has to be on the list:
https://hub.highport.space/oauth/client-metadata.json
Without it, every exchange fails with AppNotAllowed. No retry helps and no other member can grant around it, so the space page states the remedy and offers no button: Add https://hub.highport.space/oauth/client-metadata.json to the space's app access, then grant again. Until then Highport cannot read it, and retrying will not help.
The fix is the space's Settings tab. Allowed client IDs takes one identifier per line. Add the line above and save. Saving re-checks Highport's access immediately instead of leaving you to find out at the next change.
That field is loaded from your server exactly as it stands, and nothing corrects it for you. The note under it asks you to keep this line, and keeping it is your job. The one place the line is filled in automatically is New space, where choosing an allow list creates the space with it already there.
Creating a space
Spaces → New space. Two steps that report separately: the space is created on your server, then connected to Highport.
There is no name field. Your server mints the key, and it is a TID, the timestamp-shaped identifier AT Protocol uses for records, and not anything you pick. Naming the space is your server's business, and a name you give it there never changes its address here.
Three questions, and all are enforced by your server and not by Highport:
| Field | Options | What it decides |
|---|---|---|
| Who can read it | Member list (default), Public | Whether your server consults the member list, and each member's read access, when somebody asks to read the space |
| Whose writes it keeps | Member list (default), Public | Whose writes into the space your server keeps track of and passes on. It does not stop anyone writing into their own repository under the space's name, and it does not change what Highport serves, which is only ever your own records |
| App access | Open (default), Allow list | Whether your server checks the asking application's identifier |
Choose Public deliberately. It means your server admits anyone who asks, so any signed-in Highport identity that requests access to your space gets it, and can then list that space's sites, tiles and activity inside Highport for thirty days. Switching back to Member list stops new admissions and does not end the ones already granted.
If the create succeeds and the connect fails, the page says so and submitting again connects it. Nothing is created twice.
You can hold ten connected spaces per account. That ceiling and every other one is on limits and quotas.
Importing a space you already have
Spaces → Import space. Nothing is copied and nothing is created. Import here means this space already exists on your server and Highport is not reading it yet.
You supply nothing. The screen reads the spaces of Highport's type your server holds, subtracts the ones Highport already reads, and lists what is left with an Import button on each.
There is a third way in. The Publish from selector on the new-site form offers New space…, which creates and connects one when you submit.
What is inside one
A space's page is at /spaces/{authority}/{key} and has five tabs.
| Tab | Shows |
|---|---|
| Sites | Every domain registered to this space, with its registration status |
| Tiles | The tiles published into this space, which only sites in this space can bind |
| Members | Your server's member list, read live |
| Activity | What has happened to Highport's copy: grants, syncs, disconnections |
| Settings | The space's policy and app access, read from your server and written back to it |
Settings reads from your server and not from Highport's copy, because Highport's copy is a mirror taken at the last sync, and a form that edited a mirror would show you the value you just set being replaced by a stale one.
The Spaces list shows the spaces you are the authority of. A member reaches a space they can read by being handed its link.

Admitting people
The Members tab is your server's list, and the two controls on it write to your server rather than to Highport. Typing a handle or a DID into Add a member admits one person; Remove beside a name takes one away.
Each member has two kinds of access, and each has its checkbox beside their name:
- Read lets them open the space. It is also what lets their visits keep Highport's access alive: a member without read access is refused when they open the space, and so is the grant their visit would have made.
- Write decides whether your server keeps and passes on what they write into the space. Highport never serves another member's records either way.
A new member gets both unless you uncheck them. Adding somebody who is already a member replaces their access rather than doing nothing. A handle is resolved to the account it names before anything is written, and it has to resolve in both directions — the handle has to point at the account and the account has to claim the handle back — or it is refused.
From a file
Twenty readers is not twenty turns of a text field, so the Members tab takes a text file: Add everyone in a file, and Remove everyone in a file. One identity per line, written any of four ways.
# a comment line, and blank lines, are skipped
alice.example.com
@bob.example.com
did:plc:iu5fzdrrfrc6kk7vmmatvin2
did:web:example.com # a trailing comment is skipped too
Anything else is reported against its line and not applied: an at:// in front of a handle, a DID method that is not plc or web, a bare word with no dot in it, two names on one line. The same line twice is applied once. The file never reaches Highport — it is read in your browser, and what leaves is one call per identity to your own server.
There is no preview. The run starts when you choose the file and applies as it goes, removals included. What you get is a report, line by line, saying what happened to each one — applied, access updated, already a member, not a member, could not resolve, malformed, or refused by your server. Read it: the run is not a transaction and it can stop halfway. If your server asks for a pause, the run stops on the line it got to and says which, and everything below it is reported as not attempted. Choosing the same file again picks up where it left off, because admitting somebody twice does nothing and removing somebody who is already gone is reported rather than fatal.
A line in an add file grants read and write. A member already on the list with one of them withheld is given both, and the report says access updated for that line. To give somebody less than both, change their checkboxes on the Members tab instead.
The report is the only record. Highport keeps no member list and no history of one, so leaving the page loses it.
Removing by handle is checked twice
A handle can move to another account. A removal list written in March and run in June may name somebody entirely different, and removing the wrong member is silent — nothing fails, your space is just quietly short one reader.
So a removal line written as a handle is applied only if it resolves both ways and the account it names is already a member of this space. Anything else is reported and skipped. A removal line written as a DID is applied as given, because a DID cannot move.
Adding is not held to the same standard, since the worst case is somebody admitted who can read nothing they could not read before. A DID on an add line that does not resolve today is admitted anyway, with a note: membership is a DID operation, and a DID that does not resolve today may resolve tomorrow.
There is no replace, and no export
Nothing here writes a file, and no operation takes a file as the intended final state of your member list. A file is only ever a list of people to add, or a list of people to remove — so a file that is missing half its lines can add fewer people than you meant, and can never empty your space.
Both limits — how many identities one file may name, and how large the file may be — are stated on the screen before you choose one. A file over either is refused whole rather than applied as far as the limit and abandoned.
Registering a domain to a space
The registration form's Publish from selector is where you choose: My repo, one of your connected spaces, or New space…. Your DNS records are identical either way, and the ownership record carries your own account in both cases, because only a space's authority registers domains to it. Everything on bringing your domain alongside applies unchanged.
A domain lives on one side or the other
A domain registered to your account cannot serve a record from a space, and a domain registered to a space cannot serve one from your account. Both directions are refused when the record is indexed, not when it is written, so the symptom is a record that never activates:
| What you published | Where the domain is registered | The refusal |
|---|---|---|
| From your public account | To a space | DomainNotOwned — "it is registered to the space at://…, which serves it instead" |
| From a space | To your public account | DomainNotInSpace — "it is registered to an identity's public repo" |
| From one space | To another | DomainNotInSpace — "it is registered to at://…" |
Moving a domain across takes two steps: release it, then register it again on the other side. Releasing is on editing, deleting and moving on.
Publishing into a space
The site editor does not publish into a space, and nothing on the screen says so. Everything up to the write works. The editor reads the record out of the space, Get it ready validates it as a record in that space, and the panel reads Ready to publish. That is deliberate, because a space record can never report a prepared snapshot and the panel treats that as normal instead of as a failure. The Publish button is enabled, and the handler behind it stops unless validation prepared a snapshot, so no request goes out and nothing is written. Treat the button as unreliable for a space and write the record yourself. The three calls are below.
The tile builder never offers a space in the first place. It has no publish into choice, sends no space with anything it writes, and publishes into your own account even when you opened it from a site kept in a space.
Writing the record yourself
Three calls. The first two are the ordinary publishing sequence with one field added, and validate before you write is that sequence in full.
1. Validate, with space. space.highport.manage.validate takes an optional space, and you must be that space's authority. Anybody else gets NotSpaceAuthority, and SpaceNotFound is the answer for a space Highport holds no row for. The answer carries the record's AT-URI, its at:// address, along with its CID and the record as Highport reads it, exactly as for a public record. The URI is the permissioned record form, whose fourth part names the account whose records these are — the space's own authority, because Highport reads one account's records inside a space:
at://{authority}/space/space.highport.sites.space/{key}/{authority}/space.highport.sites.site/{domain}
Validation does not prepare. A file in a space becomes readable to Highport only once a committed record in that space points at it, so there is nothing for Highport to fetch ahead of your write. prepared comes back false with reason: "SpacePreparationUnsupported" whatever else is true of the record, and the check to publish on is valid: true on its own, not valid and prepared together. Everything else validation does, it still does, and it still tells you what is wrong with the record.
2. Write the record validation returned, unchanged, with com.atproto.space.putRecord at the space host, the authority's PDS, which for your own space is your own server. Not com.atproto.repo.putRecord at your account. The record key is the hostname, as it is anywhere else. That call takes no swapRecord, so the conditional republish, meaning write this only if the record is still the one I validated, is not available for a record in a space.
3. Then have Highport pull it. Make sure Highport's access is live, which opening the space page does, and press Sync now. The call under that button is space.highport.manage.syncSpace with {space}, and it is the authority's. Without it, the write is picked up when your server's notification arrives, and a check every fifteen minutes catches anything that notification missed.
Between your write and that pull, space.highport.manage.getIndexStatus can already answer for the record: a valid space validation leaves a row behind for exactly that window.
Tiles kept in a space
A tile published into a space is listed in one place, the Tiles tab on the space's page, which is space.highport.manage.listSpaceTiles and the only directory of space tiles there is. It renders their names and offers no way to use one.
They are deliberately absent everywhere else: from the public tile directory, from space.highport.sites.getTile, and from the picker in the site editor, which asks for the public listing and takes no space at all, so a tile you published into your space can never appear in it. A space keeps its records private while the bytes it publishes are public, and a space tile's name, description and icon in a public listing would be a disclosure.
Binding one is therefore a record you write yourself. Put the tile's space-form AT-URI and the CID of the version you want into the site record's source, in the shape using a tile on your site shows for a public tile. Only a site in the same space may bind it. A site in your public account naming a space tile is refused at indexing with TileNotFound, which is also the answer for a tile that does not exist, so nothing outside the space learns that it is there.
The other direction is a disagreement between two halves of Highport. The Tiles tab, on a space holding no tiles of its own, says that sites in the space can still use any public tile, and the indexer agrees. validate does not. It compares the tile's space against the site's and answers TileNotInSpace when they differ, and a public tile is in no space at all. So validation refuses a binding the indexer would accept.
Taking a site down
Deleting the site record is how one domain in a space stops serving without anything else being given up, and it is the one route with no button behind it.
The Delete button on the site's own page does not work for a site published from a space, and it reports success anyway. It deletes from your own account, which is not where a space's record is, and deleting a record that was never there is not an error. Editing, deleting and moving on has the rest of that.
Delete the record where it actually lives, with com.atproto.space.deleteRecord at the space host, the authority's PDS. There is no Highport method for this, and nothing in the Hub makes the call:
POST https://pds.example/xrpc/com.atproto.space.deleteRecord
Authorization: DPoP <your session's access token>
DPoP: <proof, bound to your session's key>
Content-Type: application/json
{
"space": "at://did:plc:zqcffbaxaz5ltrz6i2nnfzmf/space/space.highport.sites.space/3lbf2kx7yz22k",
"repo": "did:plc:zqcffbaxaz5ltrz6i2nnfzmf",
"collection": "space.highport.sites.site",
"rkey": "example.com"
}
The account, space key and hostname above are examples. All four fields are required. repo is the account whose records these are, the space's authority, since the authority's are the only ones Highport reads. It has to be the account the session belongs to: anything else is refused 403 InvalidRequest, repo must equal the authenticated subject. The record key is the hostname, so the delete names its target directly. A success is 200 and an empty object, and the call is idempotent. It succeeds whether or not the record was there, so a clean answer is not evidence that anything was removed.
No extra consent is involved. The permission you granted at sign-in already covers this, and it is the same grant that writes a record into a space. What that grant covers lists all of it.
Then have Highport pull the space, exactly as after a write: Sync now on the space page, or space.highport.manage.syncSpace with {space}. Your server's notification may arrive first, and the fifteen-minute check catches it either way. Delete, then sync, is the order that does not leave you waiting.
When the delete is seen, the domain drops back to verified: registered to you, proven, serving nothing. The registration, the DNS record and the certificate are untouched, and publishing a record again at the same key starts it serving with no re-verification. The Activity tab will not mention any of this, because nothing writes an event for a record deleted or a site switched off inside a space.
Two blunter things also stop the page, and both are in the Hub. Releasing the domain does it in one step, but it gives the name up and leaves the record in the space unused. Disconnecting or deleting the space takes down every site in it, and both are below.
Leaving
Two different things, both under Danger zone on the space page, both asking you to type the space's key exactly to confirm.
Disconnect
Highport stops reading the space, forgets its credential and its key, and takes down the sites it was serving from it. The space and every record in it survive on your server, untouched. Connecting it again resumes: the same registrations serve again, with no DNS work and no re-verification.
The domains are not released. Each one falls back to verified. Still registered to you, still proven, still unavailable to anybody else, and no longer serving. There are exactly two ways out of that state: connect the space again, or release the domain and register it again.
The Danger zone says Highport "releases the domains it served", and the paragraph above is what that sentence means. Nothing about the registration is given up, and the name does not become available to anyone.
Delete
Deleting the space destroys it on your server, along with every site and tile record in it. Domains registered to it stop serving. Highport finds out, marks the space deleted, and purges its copies behind you. This cannot be undone.
In both cases the stored bytes go under the ordinary grace period instead of being deleted at once, because they are addressed by their content and may be shared with another site.
Site owners · 7 of 12