Summary
Every part of cloud/network-policy.mdx is written on the premise that a new box starts private. The implementation does the opposite: a box is created with public = true, so its ports are reachable by anyone holding the preview URL until you explicitly close them.
This is not one wrong sentence — the page's whole mental model ("start private → escalate → reserve the public flag for deliberate publishing") is inverted relative to actual behaviour. Roughly ten passages depend on the wrong premise, and the cloud/vs-opensource.mdx comparison table is missing the difference entirely, which is where a self-hosted user migrating to Cloud would most likely look.
Ground truth (implementation)
Three facts from boxlite-ai/boxlite (main) that together define the actual behaviour:
1. The create path defaults public to true — apps/api/src/box/services/box.service.ts:286
box.public = createBoxDto.public ?? true
2. The entity/DB default is the opposite (false) — apps/api/src/box/entities/box.entity.ts:111-112
@Column({ default: false, type: 'boolean' })
public = false
So the column default never takes effect on the normal create path; :286 overrides it. (Worth a look on the BoxLite side — the two defaults disagreeing is at least confusing, and it made the intended behaviour hard to determine from code alone.)
3. Inbound on the /v1 REST surface is expressed as network.inbound.mode, not public — apps/api/src/boxlite-rest/dto/create-box.dto.ts:84-87
// mode="enabled" means services the box exposes are publicly reachable;
// mode="disabled" means private.
Observed behaviour (matches the code)
Freshly created box, a service on 8080, no visibility call made, requests from a clean client (no cookies, no headers, no redirects followed):
GET /api/box/{id} → "public": true ← created public
anonymous GET <preview-url>/ → 200 + real response body
POST /api/box/{id}/public/false → 201
anonymous GET <preview-url>/ → 307 ← now closed
POST /api/box/{id}/public/true → 201
anonymous GET <preview-url>/ → 200
Two related observations while confirming this:
POST /v1/boxes with {"public": false} is silently ignored — the created box still reports "public": true. The /v1 create DTO models inbound as network.inbound.mode, so a public key in that request body is simply dropped. There is currently no documented way to create a closed box in one call; you create it (already reachable) and then close it.
- The SDK cannot read visibility at all —
BoxInfo has no public attribute, so rt.list_info() cannot answer "which of my boxes are reachable". Only GET /api/box/{id} exposes it. (Filed separately on the BoxLite side.)
Verified against a BoxLite Cloud API endpoint; SDK built from boxlite main.
Documentation passages that are wrong
All in cloud/network-policy.mdx (line numbers approximate):
| Line |
Current text |
Why it's wrong |
| 4 |
description: "keep it private, share one port through a signed link, or make it public" |
"keep it" presumes it starts private |
| 38 |
isPublic table: "false returns it to private" |
"returns it to" presumes private was the original state |
| 51 |
"Be clear-eyed about what you just did. A public box drops the credential requirement in front of it: anyone who has a URL can reach the service…" |
Framed as the consequence of turning the flag on. It is in fact the description of the default state |
| 87 |
Heading: "Present the token when the box is private" |
The default state needs no token |
| 89 |
"A private box's preview URL is not open, so a request has to carry the token" |
Default state serves anonymous requests |
| 103 |
"The query-parameter form is what makes a private preview URL openable in a browser" |
It is already openable |
| 105 |
"A request that carries no credential the proxy accepts is redirected to sign in" |
Anonymous requests get 200, not a sign-in redirect |
| 160–162 |
Decision table — all three rows begin "Private box…" / "Public box" |
Presents private as the starting point |
| 164 |
"The order matters: start private, escalate to a signed URL when a human needs to see something, and reserve the public flag for a box whose contents you would publish deliberately." |
The most direct contradiction: the order is the reverse |
| 251 |
Troubleshooting: "Fetching a preview URL returns unauthorized → The box is private, and the request carried no credential" |
A new box is not private, so this diagnosis misleads |
And one omission, arguably the highest-impact of all:
| File |
Problem |
cloud/vs-opensource.mdx:13-27 |
The Cloud-vs-open-source table covers auth, exec shape, storage, snapshots, lifecycle, limits, cost — but not inbound reachability. The "Exposing a service" row (:22) only says Cloud gives you "a tunnel to a guest port, which carries a public URL". A self-hosted user carries over the mental model "nothing is reachable unless I forward a port", which does not hold on Cloud. |
Also minor: :13 prerequisites say write access is needed "for the public flag", implying permission is only relevant when opening a box. The same endpoint and permission are needed to close one, which is the security-relevant direction.
Suggested changes
1. Flip the page's framing rather than patching sentences. Because ~10 passages inherit the premise, sentence-level edits will leave stale assumptions behind. Suggested new spine: "A box ships reachable — close what you do not intend to publish." Concretely:
- Rewrite
:164 as the reverse order, e.g. "A new box's ports are reachable by anyone holding its preview URL. Close anything you do not intend to publish with POST /api/box/{boxIdOrName}/public/false, and reopen it deliberately when you want it published."
- Move the substance of
:51 up into the page intro and reframe it as a description of the default state, not of the flag's effect.
- Rewrite the
:87–105 block so the token forms are presented as "how to reach a box you have closed", not "how to reach a private box".
- In the decision table (
:160–162), make the first row the default state, and label the others as deliberate departures from it.
- Fix the troubleshooting row (
:251) so the diagnosis reads "the box has been closed with public/false".
- Adjust
:38 ("returns it to private" → "closes the box to unauthenticated access").
2. Add an inbound-reachability row to cloud/vs-opensource.mdx. Something like:
| Dimension |
Open source (self-hosted) |
Cloud |
| Inbound reachability |
Nothing is reachable from outside unless you forward a port |
A new box's ports are reachable by anyone holding its preview URL. Close a box with POST /api/box/{boxIdOrName}/public/false. |
3. State how to create a closed box — or state that you cannot. Right now the only documented sequence is create-then-close, which leaves a window during which the box is reachable. If POST /v1/boxes gains a working way to express this (the /v1 DTO already models inbound as network.inbound.mode, and mode: "disabled" is documented as meaning private), the page should show it. Until then it's worth saying plainly that closing is a second call, so readers can decide whether that window matters to them.
4. Document how to read the current state. The page documents POST …/public/{isPublic} for setting visibility but never mentions that GET /api/box/{boxIdOrName} returns a public field. Given the default, "how do I audit which of my boxes are reachable" is a question readers will have.
Context on the default itself
To be clear about scope: I'm told the reachable-by-default behaviour is intentional on the BoxLite side — a box should be easy to build and publish — so this issue is not asking for the behaviour to change. It's asking for the docs to describe it, since visibility is a security-relevant property and the page currently teaches the opposite.
Related: #34 covers the signed-preview-URL port restriction on the same page, and includes a shorter version of this default-visibility note. Happy to have this issue supersede that section of #34 so the two don't overlap.
Summary
Every part of
cloud/network-policy.mdxis written on the premise that a new box starts private. The implementation does the opposite: a box is created withpublic = true, so its ports are reachable by anyone holding the preview URL until you explicitly close them.This is not one wrong sentence — the page's whole mental model ("start private → escalate → reserve the public flag for deliberate publishing") is inverted relative to actual behaviour. Roughly ten passages depend on the wrong premise, and the
cloud/vs-opensource.mdxcomparison table is missing the difference entirely, which is where a self-hosted user migrating to Cloud would most likely look.Ground truth (implementation)
Three facts from
boxlite-ai/boxlite(main) that together define the actual behaviour:1. The create path defaults
publictotrue—apps/api/src/box/services/box.service.ts:2862. The entity/DB default is the opposite (
false) —apps/api/src/box/entities/box.entity.ts:111-112So the column default never takes effect on the normal create path;
:286overrides it. (Worth a look on the BoxLite side — the two defaults disagreeing is at least confusing, and it made the intended behaviour hard to determine from code alone.)3. Inbound on the
/v1REST surface is expressed asnetwork.inbound.mode, notpublic—apps/api/src/boxlite-rest/dto/create-box.dto.ts:84-87Observed behaviour (matches the code)
Freshly created box, a service on
8080, no visibility call made, requests from a clean client (no cookies, no headers, no redirects followed):Two related observations while confirming this:
POST /v1/boxeswith{"public": false}is silently ignored — the created box still reports"public": true. The/v1create DTO models inbound asnetwork.inbound.mode, so apublickey in that request body is simply dropped. There is currently no documented way to create a closed box in one call; you create it (already reachable) and then close it.BoxInfohas nopublicattribute, sort.list_info()cannot answer "which of my boxes are reachable". OnlyGET /api/box/{id}exposes it. (Filed separately on the BoxLite side.)Verified against a BoxLite Cloud API endpoint; SDK built from
boxlitemain.Documentation passages that are wrong
All in
cloud/network-policy.mdx(line numbers approximate):isPublictable: "falsereturns it to private"token"200, not a sign-in redirectAnd one omission, arguably the highest-impact of all:
cloud/vs-opensource.mdx:13-27execshape, storage, snapshots, lifecycle, limits, cost — but not inbound reachability. The "Exposing a service" row (:22) only says Cloud gives you "a tunnel to a guest port, which carries a public URL". A self-hosted user carries over the mental model "nothing is reachable unless I forward a port", which does not hold on Cloud.Also minor:
:13prerequisites say write access is needed "for the public flag", implying permission is only relevant when opening a box. The same endpoint and permission are needed to close one, which is the security-relevant direction.Suggested changes
1. Flip the page's framing rather than patching sentences. Because ~10 passages inherit the premise, sentence-level edits will leave stale assumptions behind. Suggested new spine: "A box ships reachable — close what you do not intend to publish." Concretely:
:164as the reverse order, e.g. "A new box's ports are reachable by anyone holding its preview URL. Close anything you do not intend to publish withPOST /api/box/{boxIdOrName}/public/false, and reopen it deliberately when you want it published.":51up into the page intro and reframe it as a description of the default state, not of the flag's effect.:87–105block so the token forms are presented as "how to reach a box you have closed", not "how to reach a private box".:160–162), make the first row the default state, and label the others as deliberate departures from it.:251) so the diagnosis reads "the box has been closed withpublic/false".:38("returns it to private" → "closes the box to unauthenticated access").2. Add an inbound-reachability row to
cloud/vs-opensource.mdx. Something like:POST /api/box/{boxIdOrName}/public/false.3. State how to create a closed box — or state that you cannot. Right now the only documented sequence is create-then-close, which leaves a window during which the box is reachable. If
POST /v1/boxesgains a working way to express this (the/v1DTO already models inbound asnetwork.inbound.mode, andmode: "disabled"is documented as meaning private), the page should show it. Until then it's worth saying plainly that closing is a second call, so readers can decide whether that window matters to them.4. Document how to read the current state. The page documents
POST …/public/{isPublic}for setting visibility but never mentions thatGET /api/box/{boxIdOrName}returns apublicfield. Given the default, "how do I audit which of my boxes are reachable" is a question readers will have.Context on the default itself
To be clear about scope: I'm told the reachable-by-default behaviour is intentional on the BoxLite side — a box should be easy to build and publish — so this issue is not asking for the behaviour to change. It's asking for the docs to describe it, since visibility is a security-relevant property and the page currently teaches the opposite.
Related: #34 covers the signed-preview-URL port restriction on the same page, and includes a shorter version of this default-visibility note. Happy to have this issue supersede that section of #34 so the two don't overlap.