DEV Community

Cover image for The half of California's AB 723 that nobody implements
kadirhan
kadirhan

Posted on

The half of California's AB 723 that nobody implements

`California's AB 723 has been in force since January 1, 2026. It amends
Business & Professions Code ยง 10140.8 and it applies to any real estate
listing image that has been digitally altered. Virtual staging is the obvious
case, but the definition is wider than that.

The rule has two parts:

  1. A statement that the image has been altered, "reasonably conspicuous" and placed on or adjacent to the image.
  2. A link to a publicly accessible URL, or a QR code, that includes and clearly identifies the original, unaltered image.

Everyone builds the first part. It is a text label on a photo, an afternoon of
work. The second part is a small piece of infrastructure: a permanent public
URL, per image, that outlives the tab the agent had open when they exported.

I build a virtual staging product, so I had to ship both. This is how the
second part is put together, and the one thing I got wrong.

What counts as altered

Worth getting right before writing any code, because it decides which of your
features need the label and which do not. Subsection (b)(2) carves out ordinary
photo editing.

Covered:

  • Adding furniture, rugs, art or decor
  • Removing furniture, clutter or personal items
  • Changing paint, flooring or wall finishes
  • Sky replacement and day to dusk
  • Greening or reshaping lawns and landscaping
  • Anything that changes the facade or the property itself

Not covered:

  • Exposure, lighting, white balance, color correction
  • Sharpening
  • Straightening, cropping, angle

In the codebase that line is a set, and the two omissions are deliberate:

ts
export const TOOLS_ALTERING_LISTING_IMAGES = new Set([
"virtual-staging",
"sky-replacement",
"day-to-dusk",
"grass-greener",
"declutter",
"object-remover",
]);

image-enhancer is out because exposure and white balance are precisely what
the statute excludes. A floor plan generator is out because a diagram is not an
altered photograph. Attaching a legal claim to a feature the law does not cover
is not a harmless extra: it is the fastest way to make the rest of your
compliance copy untrustworthy.

Part one: burn the label into the pixels

The tempting implementation is a DOM overlay. It is also wrong. The photo does
not stay in your app. It gets downloaded, uploaded to an MLS, dropped into a
PDF flyer, pasted into Instagram. Anything you render in HTML is gone by then.

So the label is composited server side with sharp, as an SVG layer over the
final buffer:

`ts
const padding = Math.round(width * 0.03);
const fontSize = Math.round(Math.max(16, width * 0.02));

const labelSvg = buildLabelSvg(text, style, labelWidth, labelHeight, fontSize);

sharpInstance = sharp(
await sharpInstance
.clone()
.composite([{ input: Buffer.from(labelSvg), top, left, blend: "over" }])
.toBuffer(),
);
`

Three details that cost me time:

Scale everything to image width. A 16px label is conspicuous on a 1024px
export and invisible on a 4096px panorama. Every dimension here is a fraction
of width, never a constant.

Escape the text before it reaches the SVG. User supplied label text goes
into an XML document. &, < and > get replaced or sharp throws on a
malformed SVG, which in an export pipeline surfaces as a corrupt download with
no obvious cause.

Clamp the position. A long label at a large font on a small image will
happily render outside the canvas, and sharp errors rather than cropping. Fit
the box to the image first, then clamp the offset.

The composite is wrapped in its own try/catch that logs and continues. That is
a judgment call about which failure is worse: an export that dies, or an export
that quietly ships without the label. Given the label is the legally required
part, silently continuing is arguably the wrong default and it is on my list.

Part two: a permanent link to the original

This is the part with actual state behind it. Each image in a project can mint
one share token:

ts
if (!projectImage.publicShareToken) {
projectImage.publicShareToken = randomUUID();
await project.save();
}

The token lives on the image subdocument, not on the project. The statute talks
about "the original, unaltered image" for a specific altered image, so a project
level link that lands on a gallery of twelve rooms does not clearly identify
anything. One token per image, minted lazily, and idempotent: calling it twice
returns the same URL, because the agent will paste that URL into an MLS field
and it has to survive being re-copied a month later.

The token resolves to /compare/[token], a page with no auth that shows the
original next to the staged version and states, in plain text, that the image
was generated.

The robots decision

That page carries other people's listing photos, so it must not be indexed.
But the naive noindex, nofollow turns out to be a real mistake:

ts
robots: {
index: false,
follow: true,
}

Every disclosure link on the internet points at this page. That is what the
statute asks agents to publish. With follow: false all of it dead ends. With
follow: true the photos still stay out of search, and the links that
compliance produces still reach the rest of the site. Two flags, opposite jobs,
and it is easy to set them together out of habit.

QR codes, because print exists

Subsection two allows a QR code, and the reason is obvious once you have held a
flyer. Print cannot carry a hyperlink.

ts
const compareUrl = new URL(
/compare/${token}, req.url).toString();
const png = await QRCode.toBuffer(compareUrl, { type: "png", width: 512, margin: 2 });

One guard worth copying: the route resolves the token against the database
before generating anything. Otherwise you have shipped a public, unauthenticated
QR generator that will encode any string a stranger sends it, hosted on your
domain.

What I got wrong

All of the above works. Almost nobody uses it.

I measured it on production before writing the next feature on top: across every
image eligible for a share link, fewer than one percent had ever minted one. The
number of distinct users who had minted one was one, and it was probably me,
testing.

The reason is not that the feature is broken. It is that the compliance link
sits behind a button, inside a share panel, next to a download button that works
fine without it. Two clicks that the workflow does not require. An agent
exporting eleven photos before a Monday listing does not go looking.

The lesson generalizes past real estate: a compliance feature that depends on
someone finding it is not a compliance feature.
If the on-image label is
switched on, the link to the original is not optional under the statute, so the
token should be minted at export time and handed over with the file. Making the
user assemble the two halves themselves is a design that produces a
demonstrably compliant product and non-compliant listings.

That change touches the export flow, so it is not shipped yet. But the
measurement is the useful part, and it took one script to get. Before building
the layer on top of a feature, check whether anyone reaches the feature.

Reference

I keep the rule text, what it covers, the NAR Code of Ethics articles that apply
regardless of state, and copy-paste disclosure wording on a single page:
getquickstaging.com/virtual-staging-disclosure

Four other states come up constantly in trade coverage. Every one of them is
listed there as unverified, with no summary of what it supposedly requires,
because I could not find the primary source. If you have one, I will add it.

Not legal advice. Ask your broker or your attorney.`

Top comments (0)