Tools
2 Oct 2026

Client-Side vs Server-Side Access Control for Web Apps

Client-side access control changes what the interface shows. Server-side access control decides what protected data the requester receives. If a private message has already arrived in HTML, JSON, or a client component's props, hiding it behind a countdown does not restore confidentiality. The decisive check belongs at a trusted boundary before the content is returned.A future-reveal page makes the difference easy to see. The visitor can possess a valid URL today without being allowed to read its message yet. A polished sealed-state animation might correctly represent that rule, or merely conceal data already delivered. You cannot distinguish the two by looking at the page. You need to inspect the responses.Official documentation was checked on 2 October 2026. The examples use local, in-memory fixtures and fake text. They illustrate an implementation boundary; they are not a security audit of Secret Letter or another deployed product.

Why is client-side authorization insufficient?

The browser is controlled by the person using it. That person can inspect network responses, change local state, disable scripts, or request an endpoint directly. A check performed only in that environment cannot be the authority for releasing information to the same person. OWASP's Authorization Cheat Sheet explicitly places access checks on the server, gateway, or another trusted enforcement layer rather than relying on client logic.Client checks still have useful work to do. A disabled button can explain why an action is unavailable. A countdown can reduce needless retries. A hidden admin menu can make the interface clearer for a normal user. These improvements guide interaction; the endpoint receiving the request must independently decide whether it is allowed.The distinction also applies without accounts. In a private-by-link system, the access rule might require a valid token and a reveal time that has passed. That policy is simpler than named-user authorization, but it still needs enforcement. A random-looking address and a browser date comparison do not establish a trusted boundary by themselves.

Can hidden HTML or React props contain the protected text?

Yes. Consider a response whose visible page says “Sealed” but whose HTML includes a hidden element containing the full message. The hidden attribute changes presentation. The response still carries the words. A blur effect, off-screen placement, or a collapsed panel has the same basic limitation when the underlying text is present.
<p>Sealed</p>
<div hidden>DEMO_PRIVATE_TEXT_9c2e</div>
<script type="application/json">
  {"body":"DEMO_PRIVATE_TEXT_9c2e"}
</script>
Our deliberately unsafe local fixture returns that structure. Reading the response as text finds the synthetic marker both in the hidden element and in the JSON. No browser rendering is needed to observe the disclosure. Replacing the hidden element with a React component that receives the full body does not solve the data-transfer problem.The current Next.js data-security guide discusses minimizing data returned from the data-access layer and warns about passing complete records into client components. Apply that boundary deliberately: return a sealed state before reveal, rather than returning the body and asking a client component to ignore it. Server-rendered origin does not make every field safe to serialize.

What should the server return before the reveal date?

It should return only the information needed for the allowed state. That might include a generic label and the reveal timestamp if those fields are intentionally public to a link holder. It should omit the message body, private excerpts, attachments, and any other fields the policy still withholds. The absence of protected data matters more than which lock icon appears above it.Use the stored reveal time and a trusted clock at the enforcement layer. Do not accept a request parameter such as now=tomorrow as proof that the date has arrived. The browser can calculate a countdown for display, but the server needs its own decision. Missing records or invalid access credentials should produce a defined unavailable response without including protected fields in error details.
function permittedPayload(record, serverNow) {
  if (!record) {
    return { state: "unavailable" };
  }
  if (serverNow < record.revealAt) {
    return { state: "sealed", revealAt: record.revealAt };
  }
  return { state: "open", body: record.body };
}
This is a policy sketch, not a production endpoint. Credential validation, record lookup, error handling, response construction, logging, and storage permissions remain necessary. The function is useful because the forbidden field is absent from its sealed result, making the intended contract explicit and easy to inspect.

What happens before, at, and after the boundary?

The complete fixture uses a stored reveal timestamp of 3 October 2026 at 12:00 UTC. Its private address contains a synthetic token; the handler validates that path before applying the date rule. The results below come from deterministic Request/Response objects, not wall-clock waits or requests to a real service.
Observed local fixture results on 2 October 2026.
Request caseHTTP statusFake body present?
Valid token, one millisecond early403No
Valid token, exact reveal time200Yes
Valid token, one millisecond later200Yes
Early request with forged now parameter403No
Unknown token after reveal404No
Simulated preview user agent before reveal403No
The status choices are part of this example's contract. Another service might use a 200 sealed shell or a generic 404 response. What matters for the confidentiality test is that the denied response does not contain the body. A successful status alone is not proof of a leak, and a forbidden status alone is not proof that its payload is harmless.The preview case simply changes a request header to a local test label. It shows that our fixture does not grant an exception based on user agent. It does not reproduce a particular platform's bot. Real systems also need coverage for alternate endpoints, storage paths, deployment settings, and errors outside this small model.

Where can a correct access check still lose its effect?

Another response path returns the same record

Checking the visible page route is insufficient if a download URL, JSON endpoint, preview image, or alternate representation returns the same protected material without the check. Put the policy near the protected data and apply it consistently to every retrieval path. Returning a smaller allowed object helps prevent callers from accidentally serializing an entire record.

A cache serves data without the intended policy

Shared caching can bypass a decision when a response is reused for the wrong request or credential. MDN's Cache-Control reference distinguishes no-store from no-cache: the latter permits storage but requires revalidation before reuse. The fixture sets no-store on every protected response. That verifies a header, not the behavior of an actual CDN or browser.Audit framework caches, edge rules, service workers, and object storage separately. Public assets can remain efficiently cached while restricted responses follow a different policy. Blanket disabling of all caching adds cost without fixing a public storage URL that bypasses authorization entirely.

Metadata discloses content that the main body withholds

A title, description, or social image can leak the subject even when the body endpoint behaves correctly. Keep denied-state metadata generic and review every field that leaves the trusted layer. Our link preview privacy analysis shows the separate distinction between revealing a private excerpt and handing a fetching service the URL itself.

Does server-side enforcement mean end-to-end encryption?

No. An application can correctly withhold a message from an early requester while its operator still has access to stored text. Access control determines which requests receive data. Encryption concerns how data is protected and who holds the ability to decrypt it. A working server check does not establish an end-to-end encryption design, anonymity, or protection from an operator compromise.Secret Letter's documented future-reveal model provides a concrete consumer requirement: no account or recipient email is needed, the link exists immediately, and the letter stays closed until its reveal date. It does not automatically send an email later. Its current basic text use is free, and the URL must be saved or shared safely.It uses private-by-link access and should not be described as end-to-end encrypted. Passwords, financial information, medical records, and critical secrets are unsuitable for this kind of casual letter service. We checked its public product documentation, not its code or private response paths; the fixtures do not establish how its enforcement is implemented. The existing Secret Letter review and limitations remains the product-evaluation page.

Can a static website enforce a private reveal date?

A publicly served static file cannot withhold plaintext it already contains. Building a page ahead of time and hiding its body with browser JavaScript does not turn the file into an access-controlled response. A static interface can call a protected backend, or a gateway can enforce access before serving a resource, but that trusted layer must actually exist.Keep that deployment distinction clear when adapting examples to React or Next.js. A server component rendered during a static export is not automatically a request-time authorization service. If the exported HTML contains the message, a later browser check arrives too late. For public announcements where early confidentiality is not required, a client countdown may be entirely appropriate; describe it as presentation behavior.

What should you verify before shipping protected content?

Start with a synthetic marker and inspect every response that could contain it: initial HTML, serialized props, data requests, metadata, and attachments. Check invalid credentials, direct requests, the exact time boundary, and a forged browser timestamp. Then inspect the deployed enforcement and caching configuration. A UI screenshot and a unit-level policy check answer useful but incomplete questions.Specify the allowed payload before building the animation. Let the interface explain the server's decision, handle failures clearly, and avoid claiming protection that the deployment cannot enforce. If your unresolved question is when a URL should become valid, our expiration versus reveal-date comparison owns that lifecycle decision. Find related product and technology coverage in the Tools category.
Useful online tools and web apps: focused reviews, honest comparisons, and practical recommendations.