Skip to content

ENG-1870 Copy Roam-origin assets into Supabase on publish - #1394

Open
maparent wants to merge 5 commits into
mainfrom
eng-1870-copy-roam-origin-assets-into-supabase-on-publish
Open

ENG-1870 Copy Roam-origin assets into Supabase on publish#1394
maparent wants to merge 5 commits into
mainfrom
eng-1870-copy-roam-origin-assets-into-supabase-on-publish

Conversation

@maparent

@maparent maparent commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Reviewer brief

This is a large PR, there are a lot of moving pieces.

There are actually five subtasks, in distinct commits, which may be easier to understand:
https://roamresearch.com/#/app/discourse-graphs/page/4o_mub_mn

Verification

There are unit tests.

Loom video

Code walkthrough (long): https://www.loom.com/share/91078efd00b14f959ebf0ba07950a4fe

Note: I mention something missing during the code walkthrough, but upon verification it's fine.

Demo: https://www.loom.com/share/ecdd8cdfaca141cfb26216ecda5cdc08

Scope check

  • [ x] Ran $scope-check against the ENG ticket and final diff.
  • Scope beyond Done When: None.
    Actually it called me on the change to return parameters of addFile, but I told it that CopiedAsset is the stage's record of what it stored, a stored asset's identity in shared storage is its hash, and the summary was previously conflating references with blobs. addFile's return is what makes that available without re-hashing. It accepted that justification as valid.

It's also pending the issue of whether the size limit should have been split between tickets, but right now it's within 1876.

Local delegated full review

  • [ X] Ran a comprehensive review of the entire final diff in a subagent with a fresh context. Use $dg-delegated-full-review when no other full-review workflow is available.

https://linear.app/discourse-graphs/issue/ENG-1870/copy-roam-origin-assets-into-supabase-on-publish

@linear-code

linear-code Bot commented Sep 4, 2026

Copy link
Copy Markdown

ENG-1870

@vercel

vercel Bot commented Sep 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
discourse-graph Ready Ready Preview Sep 10, 2026 8:47pm UTC

Request Review

@supabase

supabase Bot commented Sep 4, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project zytfjzqyijgagqxrzbmz because there are no changes detected in packages/database/supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@maparent
maparent force-pushed the eng-1870-copy-roam-origin-assets-into-supabase-on-publish branch from d058750 to 14f6b43 Compare September 4, 2026 15:18
@maparent
maparent force-pushed the eng-1870-copy-roam-origin-assets-into-supabase-on-publish branch from 14f6b43 to 6f408af Compare September 4, 2026 18:22
@maparent
maparent force-pushed the eng-1870-copy-roam-origin-assets-into-supabase-on-publish branch from 6f408af to d891c94 Compare September 4, 2026 18:50
@maparent
maparent force-pushed the eng-1870-copy-roam-origin-assets-into-supabase-on-publish branch from d891c94 to 3010d2d Compare September 4, 2026 18:55
@maparent
maparent requested a review from mdroidian September 4, 2026 19:11
@maparent
maparent force-pushed the eng-1870-copy-roam-origin-assets-into-supabase-on-publish branch from 3010d2d to 7839b4c Compare September 6, 2026 14:36
@maparent
maparent marked this pull request as ready for review September 6, 2026 15:25
@graphite-app

graphite-app Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

PR size/scope check

This PR is over our review-size guideline.

  • Recommended: ~200 lines changed
  • Acceptable limit: up to 400 lines when well-scoped/self-contained
  • Preferred file count: fewer than 5 files

Please split this into smaller PRs unless there is a clear reason the changes need to land together.

If keeping it as one PR, please add a brief justification covering:

  • What single problem this PR solves
  • Why the files/changes are coupled

devin-ai-integration[bot]

This comment was marked as resolved.

@maparent
maparent force-pushed the eng-1870-copy-roam-origin-assets-into-supabase-on-publish branch from 7839b4c to 264ccb5 Compare September 6, 2026 22:32

@mdroidian mdroidian left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See devin comments


/** Roam uploads land in its own Firebase project, which we do not control. */
const ROAM_STORAGE_HOST = "firebasestorage.googleapis.com";
const ROAM_STORAGE_BUCKET = "firescript-577a2.appspot.com";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might not be stable, probably isn't a good idea to rely on it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As also said in Linear: I suggest keeping them, but detecting new buckets. Details here.

* publish. Anything else (an image hotlinked from another site, a link to a paper) is a
* resource that was never an asset of the node, and is left alone.
*
* Matching the bucket rather than the path shape: every Roam upload lands in this one

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"lands in this one bucket" for now. Not a good idea to rely on this.

@maparent
maparent force-pushed the eng-1870-copy-roam-origin-assets-into-supabase-on-publish branch from 264ccb5 to 7e0bb26 Compare September 8, 2026 15:35
@maparent
maparent force-pushed the eng-1870-copy-roam-origin-assets-into-supabase-on-publish branch from 7e0bb26 to 46b1149 Compare September 8, 2026 15:40

@mdroidian mdroidian left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I'm going to block on storing/relying on Roam's current bucket URL. This is setting us up for failure in the future unncessarily.

Use a regex pattern (that doesn't include their specific bucket name) + roam's API to confirm it's roam's bucket.

maparent commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

I see your point. Will work on this.

maparent commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

Investigated. I assumed you meant roamAlphaAPI.file.get. I'm not using it now, and that's a mistake for encrypted graphs; so worth the pushback. BUT it does not distinguish roam buckets from arbitrary firebase buckets, so it does not do what you want (gate on Roam content.) This also has security implications, as noted.

Copy link
Copy Markdown
Member

How are you getting the filename?

Copy link
Copy Markdown
Member

Why are we not using the roam API to get the files?

Copy link
Copy Markdown
Member

Why wouldn't using roamAlphaAPI distinguish roam buckets from arbitrary firebase buckets? If you use roamAlphaAPI on a legitimate file in a roam bucket, you will get a success response. If you use roamAlphaAPI on a non roam bucket, I'm assuming you will get an error response (unauthorized). This would solve the security implication, yes?

mdroidian commented Sep 9, 2026

Copy link
Copy Markdown
Member

Even if the roamAlphaAPI just performs a standard fetch, it isn't clear what the security implication actually is compared to downloading bytes from roam's bucket, or an arbitrary obsidian file.

Base automatically changed from eng-1864-define-cross-app-asset-reference-contract to main September 9, 2026 13:26
@maparent
maparent force-pushed the eng-1870-copy-roam-origin-assets-into-supabase-on-publish branch from 9e85cbc to 5e50703 Compare September 9, 2026 13:26

maparent commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

Why not using roam API: I did not think of the encryption case, and Claude chose a common API. Working on reverting that. It also means we'll get the filename and mimetype from the file. However, I checked that the lastModified that we get from file.get is spurious (always now) whereas the one we get from metadata is valid and, more important, stable, so keeping the metadata call for now. I thought it was reliable for size, but again only for unencrypted graphs. There's a shortcut that depended on that, becomes conditional.
As to "Why wouldn't using roamAlphaAPI distinguish roam buckets from arbitrary firebase buckets?", no idea why, this was an empirical test. I took a roam URL, altered a character in the bucket name, and though it came back with a 404, I did see in the network pane that it did attempt to fetch it. (Non-firebase URLs throw, at least.) So again, we cannot rely on Roam to distinguish roam bucket from non-roam bucket, so I maintain the analysis that we need to record other bucket usage, but refuse it at the outset for security reasons.

maparent commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

BTW making assumptions about encrypted graphs; if you could create one and give me access, I'd love to check some of them. But I doubt it would change anything deeply.

@maparent
maparent force-pushed the eng-1870-copy-roam-origin-assets-into-supabase-on-publish branch from 5e50703 to df766ec Compare September 9, 2026 15:57
@maparent
maparent force-pushed the eng-1870-copy-roam-origin-assets-into-supabase-on-publish branch from df766ec to cf6023c Compare September 9, 2026 18:04
@maparent
maparent force-pushed the eng-1870-copy-roam-origin-assets-into-supabase-on-publish branch from cf6023c to ab7a424 Compare September 9, 2026 18:11
@maparent

maparent commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

How are you getting the filename?

From the metadata, in fetchAssetDescriptor.

Why are we not using the roam API to get the files?

Corrected.

Now identifying roam buckets by URL as agreed.

@maparent
maparent requested a review from mdroidian September 9, 2026 18:14
@maparent
maparent removed this pull request from stack #1393 September 10, 2026 14:42
@maparent
maparent added this pull request to stack #1430 September 10, 2026 14:43
@maparent
maparent force-pushed the eng-1870-copy-roam-origin-assets-into-supabase-on-publish branch from ab7a424 to afdcfb2 Compare September 10, 2026 20:44

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 new potential issues.

3 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)

Devin Review

Comment on lines +70 to +74
const { data, error } = await client
.from("my_file_references")
.select("source_local_id, filepath, filehash, source_path")
.eq("space_id", spaceId)
.in("source_local_id", sourceLocalIds);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Asset reconciliation stops at 1,000

When a publish batch has over 1,000 stored references, readExistingReferences silently drops later rows. The configured PostgREST limit truncates this unpaginated query. Omitted stale references survive cleanup, while current references are downloaded again.

Learn more

PostgREST limits one response to 1,000 rows in this repository. This query combines every selected node's references, so a normal row limit applies across the whole batch rather than per node. Rows after the cap never enter existingByNode. The copy loop treats those rows as absent, and stale-reference detection cannot remove them.

Example: Publishing 600 nodes with two existing assets each requests 1,200 rows. If 200 rows are omitted, unchanged assets in those rows are fetched again; stale assets in those rows remain published.

Recommended fix: Read my_file_references through getAllPages with deterministic ordering, and propagate pagination errors before any copying or cleanup.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +72 to +82
const { hostname, pathname } = new URL(url);
if (hostname !== ROAM_STORAGE_HOST) return false;
const segments = objectSegments(pathname);
const folder = segments.slice(0, ASSET_FOLDER.length);
const graphName = segments[ASSET_FOLDER.length];
const named = segments.slice(ASSET_FOLDER.length + 1);
return (
folder.join("/") === ASSET_FOLDER.join("/") &&
// Exact, not case-insensitive: Roam preserves the graph name's casing in the URL.
graphName === window.roamAlphaAPI.graph.name &&
named.some((segment) => segment !== "")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟥 Untrusted Firebase buckets trigger asset copying

A pasted Firebase URL passes isRoamStorageUrl when its path contains the graph name, regardless of bucket. Publishing then fetches attacker-controlled bytes and stores them under the user's Supabase authority.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +97 to +108
const contentHash = await addFile({
client,
spaceId,
sourceLocalId,
fname: assetUrl,
sourcePath: asset.filename,
mimetype: asset.mimetype,
// Storage timestamps, not `File.lastModified`, which is the time of the call. The
// node's own dates are the fallback, and are stable for the same reason.
created: descriptor.createdAt ?? nodeCreated,
lastModified: descriptor.modifiedAt ?? nodeLastModified,
content: asset.content,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟨 Shared metadata exposes asset tokens

fname stores the complete Firebase URL, including its download token, in shared file metadata. Group readers can retrieve that reusable token through my_file_references.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants