When people send proposals, pitch decks, NDAs, and other client-facing documents, they usually want the recipient focused on their brand, not the software powering the experience.
That was the motivation behind a feature I recently shipped in DocBeacon: white-label document sharing for Pro users.
The idea sounds simple:
Let users attach a custom brand name, logo, and website URL to shared documents.
But once I got into implementation, it turned into an interesting mix of product design, access control, storage, validation, and fallback behavior.
This post is a short breakdown of how I approached it.
The problem
Before this feature, a shared document was still visually branded as DocBeacon throughout the viewing flow.
That was fine for some use cases, but not ideal for teams sending:
consulting proposals
investor decks
sales documents
sensitive client materials
In those workflows, brand consistency matters. If someone is sharing a high-value proposal, they usually want the experience to reinforce their credibility, not mine.
So the requirement became:
Let eligible users show their own branding across the document viewing experience, while keeping the system safe and predictable.
What the feature needed to support
At a high level, I wanted users to be able to configure:
a display name
a logo
a website URL
an on/off toggle for branding
And once enabled, that branding should appear in places like:
the document viewer header
attribution text
shared document metadata
other client-facing viewer touchpoints
I also wanted a clean fallback path. If branding data was incomplete, invalid, or the user’s plan didn’t allow the feature, the system should quietly fall back to the default DocBeacon brand.
The core design decision: resolve branding once
One of the first decisions I made was to avoid scattering branding logic across multiple rendering layers.
Instead of asking every page or component to figure out branding on its own, I used a single resolution step:
Check whether the user is allowed to use the feature
Check whether branding is enabled
Check whether enough valid branding data exists
Return either:
the user’s brand config, or
the default platform brand
In pseudocode:
**function resolveBranding(user):
canBrand = planAllowsBranding(user.plan)
hasIdentity = user.displayName OR user.logoPath
enabled = user.brandingEnabled AND canBrand AND hasIdentity
if enabled:
return {
name: user.displayName,
logoUrl: buildAssetUrl(user.logoPath),
websiteUrl: normalizeUrl(user.websiteUrl)
}
return defaultBrand**
This helped keep the rest of the system simple. Viewer pages didn’t need to know all the rules. They just consumed a resolved branding object.
Access control
This feature is only available on Pro+ plans, so access control had to be built into the feature itself, not just the UI.
That means even if someone somehow submits branding settings through an API call or stale client state, the backend still needs to enforce eligibility.
The check is conceptually simple:
canUseBranding(user):
return user.plan in ["pro", "business", "enterprise"]
I treated this as a server-side rule first, and a UI rule second.
That’s important because frontend gating alone is never enough for plan-restricted features.
Asset storage for logos
For logo uploads, I wanted a storage layer that was simple, cheap, and fast to serve publicly.
I used Cloudflare R2 for uploaded branding assets.
The flow looks roughly like this:
User uploads a PNG or JPG
Backend validates file type and size
File is stored under a user-scoped path
The app stores only the storage path in the user record
A helper builds the public asset URL when needed
In pseudocode:
uploadLogo(file, user):
assert file.type in ["image/png", "image/jpeg"]
assert file.size <= MAX_LOGO_SIZE
path = "branding/" + user.id + "/" + generateFileName(file)
storage.put(path, file)
saveUserBranding(user.id, { logoPath: path })
And later:
buildAssetUrl(path):
return CDN_BASE_URL + "/" + path
I preferred storing the path rather than a fully expanded URL in the database, because it keeps storage concerns separate from delivery concerns.
URL validation was the trickiest part
The hardest part of this feature was not rendering a logo.
It was letting users attach a website URL safely.
On the surface, “enter your website” sounds trivial. In practice, it needs guardrails.
I did not want to allow:
localhost
private network IPs
credential-based URLs like user:pass@example.com
unsafe schemes like javascript: or file:
So I treated URL handling as a normalization + validation pipeline.
The job of that pipeline is:
parse the input
normalize format
require http or https
reject unsafe hosts
reject credentials
return either a safe URL or null
In pseudocode:
normalizeUrl(input):
if input is empty:
return null
parsed = parseUrl(input)
if parsed.scheme not in ["http", "https"]:
return null
if parsed.hasCredentials:
return null
if isLocalhost(parsed.host) or isPrivateIp(parsed.host):
return null
return parsed.normalizedHref
This kind of defensive validation matters because branding fields are user-controlled input, and user-controlled input always deserves scrutiny.
Fallback behavior matters more than people think
A lot of polish in a feature like this comes from handling invalid states well.
For example:
branding is toggled on, but no logo is uploaded yet
the user entered an invalid URL
the user downgraded from Pro to Free
an asset was deleted or unavailable
branding data is partially filled out
In all of those cases, I wanted the document experience to remain stable.
So instead of letting the UI break or showing half-configured branding, I default to the platform brand whenever the custom brand can’t be resolved safely.
That gives the system a predictable contract:
Either show a fully valid custom brand, or show the default brand.
No broken middle state.
Why I like this implementation
What I like about this feature is that it makes the product less visible in the right places.
For this use case, that’s the goal.
The product should support the sender’s identity, not compete with it.
From an engineering point of view, I also like that the system is built around a few clear principles:
central branding resolution
server-side plan enforcement
explicit URL normalization
safe asset handling
reliable fallback behavior
The final result feels simple to the user, which usually means the internal rules are doing their job.
What I’d improve next
A few things I’d likely add next:
custom domains for shared links
better branding previews in settings
image processing for uploaded logos
audit logging for branding changes
more granular per-document branding controls
That would take it from “account-level branding” to a more flexible identity layer.
Final thought
This started as a branding feature, but it ended up being a good reminder that “simple UX” often sits on top of a lot of backend discipline.
If you’re building anything user-configurable and client-facing, the hard part usually isn’t just rendering the settings.
It’s defining the rules for what’s allowed, what’s safe, and what happens when the input is incomplete.
If you’re curious, the product is DocBeacon: https://docbeacon.io
Top comments (0)