This article is co-authored with generative AI. While I have cross-checked facts against official documentation where possible, errors may remain. Please verify primary sources before making important decisions.
Throughout this post, account names and IDs are written as placeholders. For example:
- GitHub Pages host:
<gh-user>.github.io(this mirror usesnakamura196.github.io)- Firebase project ID:
<firebase-pid>- JSONkeeper base URL:
<jsonkeeper-base>(this mirror designates the Cloudflare Workers version as the mainline)Substitute your own values when reading.
Demo
The live mirror is publicly accessible at the URLs below. You can open them in your browser before, or while, reading this post to see the actual behavior.
- Index page: https://nakamura196.github.io/codh-mirror/
- IIIF Curation Viewer: https://nakamura196.github.io/codh-mirror/iiif-curation-viewer/
- IIIF Curation Manager: https://nakamura196.github.io/codh-mirror/iiif-curation-manager/
- IIIF Curation Editor: https://nakamura196.github.io/codh-mirror/iiif-curation-editor/
- IIIF Curation Board: https://nakamura196.github.io/codh-mirror/iiif-curation-board/
Login (Google, etc.) → Curation editing → save → reopen all work end-to-end in the browser. The backend is the Cloudflare Workers + D1 service from this article (mainline) running alongside the PythonAnywhere Flask deployment (fallback), described in two separate posts.
A note: this is operated as a demo
The backends above (Cloudflare Workers + D1 / PythonAnywhere) are operated as a demo, not as a long-term storage service. They may be shut down without notice under any of the following:
- CODH service resumes (the mirror's mission ends)
- Operator's circumstances (operational review, Firebase project cleanup, pricing changes, etc.)
So please:
- Export (download) your saved curation JSON regularly to your own machine.
- For sustained or production-grade use, please follow this article to stand up your own equivalent backend (Cloudflare Workers + D1 and PythonAnywhere both fit inside individual free tiers).

Background — walking back the earlier "won't work" claim
In an earlier post — Setting up a dedicated GitHub Pages repository for a temporary CODH-tool mirror — I set up codh-mirror as a temporary mirror covering the long-term outage of codh.rois.ac.jp, and explicitly wrote:
Pitfall 5: The auth and save backends are not going to work
declaring login (Firebase) and curation save (via mp.ex.nii.ac.jp/api/curation/json) for IIIF Curation Viewer/Manager/Editor/Board out of scope. The reasoning was operational cost: a temporary read-only mirror should not drag in long-running services.
Some time later, after I had set up two separate JSONkeeper-compatible servers (upstream Flask on PythonAnywhere, and a rewrite on Cloudflare Workers + D1) for adjacent reasons, the logic flipped: "now that the server side is in place, I might as well enable auth and save on the mirror itself." This post is the record of that reversal and the concrete changes made on the codh-mirror repository side.
Server-side (JSONkeeper) deployment details live in two separate posts:
- Deploying JSONkeeper to PythonAnywhere's free Beginner plan using only the HTTP API (in Japanese) — Running the upstream IllDepence/JSONkeeper as-is; currently kept as a dormant reference implementation
- Rewriting JSONkeeper on Cloudflare Workers + D1 — fitting it in 360 lines with Hono and jose (in Japanese) — The mainline backend
This post is purely about how the client side (codh-mirror) plugs into those two backends.

Before / After
Before
| Item | Value |
|---|---|
| Mirror hosting | GitHub Pages https://<gh-user>.github.io/codh-mirror/ |
| Curation Viewer / Manager / Editor / Board auth | authFirebase.js hardcoded to the original codh-81041 project. With that endpoint unavailable, the OAuth flow doesn't complete |
| Curation Viewer / Manager / Editor / Player / Board save target | 9 places across index.js files (5 in Viewer alone) hardcoded to curationJsonExportUrl: 'https://mp.ex.nii.ac.jp/api/curation/json'. Currently returns 404/503 |
| iiif-curation-manager submenu links | curationViewerUrl through curationBoardUrl (4 entries) hardcoded to absolute upstream URLs http://codh.rois.ac.jp/software/iiif-curation-*/demo/ |
After
| Item | Value |
|---|---|
| Firebase project | Reused an existing kunshujo-c project. Added <gh-user>.github.io to Authorized domains |
| Firebase Web SDK config | Replaced firebaseConfig in authFirebase.js across the 4 tools (Viewer / Manager / Editor / Board) with kunshujo-c values |
| FirebaseUI behavior | Added signInFlow: 'popup' to Manager / Editor / Board's authFirebase.js (Viewer had it already) |
| JSONkeeper endpoint | Rewrote 9 occurrences of curationJsonExportUrl across 5 tools (Viewer / Manager / Editor / Player / Board) to point at the Cloudflare Workers + D1 version (<jsonkeeper-base>) |
| Manager submenu links | Switched the 4 URLs to relative paths (../iiif-curation-viewer/ etc.) targeting the sibling directories under GitHub Pages |
1. Replacing the Firebase project
The Firebase project codh-81041 that the original tools pointed to is operated upstream and untouchable from the mirror's side. A different project has to be substituted. This mirror reuses an existing personal project, kunshujo-c (so it shares a project rather than spinning up a new one).
1.1 Firebase Console setup
In the Firebase Console (https://console.firebase.google.com/project/<firebase-pid>/), two settings need attention.
Authentication > Settings > Authorized domains — add <gh-user>.github.io. By default the allowed list contains only localhost / <firebase-pid>.firebaseapp.com / <firebase-pid>.web.app, so the OAuth flow from GitHub Pages requires an explicit addition here.
Authentication > Sign-in method — enable each provider you need (this mirror enables Google / Facebook / Twitter / Email, matching the signInOptions block in the upstream authFirebase.js). For a fresh project all providers are disabled by default; forgetting this leaves the login button doing nothing.
1.2 Replacing the Web SDK config in authFirebase.js
Each tool has a same-named file authFirebase.js with the Firebase Web SDK firebaseConfig hardcoded. By design, apiKey and friends in the Firebase Web SDK are client-visible identifiers (not secrets), so you can paste the values straight out of Console > Project settings > Your apps.
- apiKey: 'AIzaSyAcsAVeIJ5l2HCWY0OlCMxP-OVXodONYqA',
- authDomain: 'codh-81041.firebaseapp.com',
- databaseURL: 'https://codh-81041.firebaseio.com',
- projectId: 'codh-81041',
- storageBucket: 'codh-81041.appspot.com',
- messagingSenderId: '230970439562'
+ apiKey: '<firebase-apiKey>',
+ authDomain: '<firebase-pid>.firebaseapp.com',
+ projectId: '<firebase-pid>',
+ storageBucket: '<firebase-pid>.firebasestorage.app',
+ messagingSenderId: '<firebase-mid>',
+ appId: '<firebase-appId>'
Four files to edit:
| File | What |
|---|---|
iiif-curation-viewer/authFirebase.js | firebaseConfig block |
iiif-curation-manager/authFirebase.js | same |
iiif-curation-editor/authFirebase.js | same |
iiif-curation-board/authFirebase.js | same |
Player doesn't save from the client (it's playback-only), so it has no authFirebase.js.
⚠
databaseURLis optional if you don't use Realtime Database, and the new project's storage bucket uses thefirebasestorage.appform. Don't carry over the old keys verbatim — paste from the values Firebase Console actually prints.
2. The signInFlow: 'popup' trap — Viewer works, Manager/Editor/Board don't complete
This was the hardest part. Just swapping firebaseConfig is not enough; Manager/Editor/Board login still doesn't complete. The symptom: clicking login pops an OAuth consent screen in a separate tab, the authorization succeeds, the tab closes, but the originating page's auth state never flips to logged-in.
The cause was a one-line difference between Viewer and the other three tools' authFirebase.js:
var uiConfig = {
+ signInFlow: 'popup',
signInOptions: [
firebase.auth.GoogleAuthProvider.PROVIDER_ID,
firebase.auth.FacebookAuthProvider.PROVIDER_ID,
firebase.auth.TwitterAuthProvider.PROVIDER_ID,
firebase.auth.EmailAuthProvider.PROVIDER_ID,
],
tosUrl: ''
};
FirebaseUI 3.x's signInFlow defaults to redirect mode for federated providers when unspecified. In redirect mode, after authorization the browser is sent through https://<firebase-pid>.firebaseapp.com/__/auth/handler and back to the original URL, and the auth state is picked up on return. But with the combination of GitHub Pages subpath (/codh-mirror/iiif-curation-manager/) plus query parameters (?lang=ja), getRedirectResult() on return doesn't always fire onAuthStateChanged — so the login looks like it succeeded but the UI never updates.
Only Viewer's authFirebase.js had signInFlow: 'popup' explicitly set (presumably added in an upstream update to Viewer specifically). That one-line difference is exactly why Viewer worked out of the box while Manager/Editor/Board didn't.

Adding the same one line to the three remaining files finally got login working uniformly across all four tools.

A diagnostic checklist
For similar symptoms, here's the decision tree:
| Observed behavior | Likely cause |
|---|---|
| Error before the OAuth consent screen ever appears | auth/unauthorized-domain (Authorized domains not configured) |
| Tab opens but never returns | OAuth provider (Google/Twitter/etc.) redirect URI not registered |
| Tab closes but originating page state doesn't change | signInFlow unspecified (= redirect mode) combined with a subpath URL ← this case |
| Login button does nothing on click | Provider disabled on the Firebase side |
3. Replacing the JSONkeeper endpoint
Point the save target away from https://mp.ex.nii.ac.jp/api/curation/json and at a JSONkeeper-compatible API (Cloudflare Workers + D1 as mainline for this mirror). The substitution covers 9 occurrences of curationJsonExportUrl across 5 tools.
| File | Occurrences |
|---|---|
iiif-curation-viewer/index.js | 5 (one per configExample.* block) |
iiif-curation-manager/index.js | 1 |
iiif-curation-editor/index.js | 2 |
iiif-curation-player/index.js | 1 |
iiif-curation-board/index.js | 1 |
A single sed -i invocation does it (and the same form in reverse rolls it back when the upstream URL returns):
find iiif-curation-* -name 'index.js' -exec \
sed -i '' -E "s|https://mp\.ex\.nii\.ac\.jp/api/curation/json|<jsonkeeper-base>|g" {} +
Two backends running in parallel
This mirror runs the Cloudflare Workers + D1 implementation (in Japanese) as the mainline and the PythonAnywhere upstream Flask version (in Japanese) as a "dormant upstream-faithful reference implementation". Switching from the client side is just changing <jsonkeeper-base> to a different URL, and rollback is one sed away too.
The Viewer's export workflow only touches POST/GET/PUT/DELETE plus the
Locationresponse header andX-Firebase-ID-Tokenverification — so the features the Workers version omits (X-Unlisted,/<id>/statusPATCH, Activity Stream pagination, garbage collection, Range sub-URLs) never come into play. The rationale lives in the Workers post.
The Workers version's design choice to not depend on the Firebase Admin SDK (it fetches and caches the Google x509 public keys from
securetoken.googleapis.comand passes them tojose'sjwtVerify) is covered in Workers post §4 "Design choice 1: Authorization — drop the Firebase Admin SDK". The reason this post's Firebase configuration can stay confined to the Web SDK config (which is fine to publish) is that no service-account key is required server-side.
Verification
After logging in, save one arbitrary curation in Viewer and confirm that the response Location header returns a URI issued by JSONkeeper.
On the Workers + D1 side, you can verify the row actually landed via wrangler d1 execute jsonkeeper --remote --command "select id, owner_uid, created_at from documents order by created_at desc limit 5".
4. Switching iiif-curation-manager's external links to relative URLs (side fix)
Manager's menu has "Open this curation in Viewer / Editor / Player / Board" links, and those URLs were hardcoded to absolute upstream paths (http://codh.rois.ac.jp/software/iiif-curation-viewer/demo/ and so on). With those endpoints currently unresponsive, clicking them lands on 404.
Under GitHub Pages the same tools sit as sibling directories, so switching to relative URLs is the cleanest option:
-curationViewerUrl: 'http://codh.rois.ac.jp/software/iiif-curation-viewer/demo/',
-curationEditorUrl: 'http://codh.rois.ac.jp/software/iiif-curation-editor/demo/',
-curationPlayerUrl: 'http://codh.rois.ac.jp/software/iiif-curation-player/demo/',
-curationBoardUrl: 'http://codh.rois.ac.jp/software/iiif-curation-board/demo/'
+curationViewerUrl: '../iiif-curation-viewer/',
+curationEditorUrl: '../iiif-curation-editor/',
+curationPlayerUrl: '../iiif-curation-player/',
+curationBoardUrl: '../iiif-curation-board/'
When the upstream returns, the same change is one sed away from being undone — the relative paths just locally point at the mirror's own copies in the meantime.
Troubleshooting
Pitfalls I actually hit, grouped by symptom:
"Login button exists but nothing happens / no OAuth consent screen"
→ The provider is disabled in Firebase Console. Enable all required ones per §1.1.
"auth/unauthorized-domain shows up in DevTools console"
→ Authorized domains not configured. Add <gh-user>.github.io per §1.1.
"Get past the OAuth consent screen, but never end up logged in"
→ Most likely the signInFlow: 'popup' issue from §2. Adding the one line fixes it.
"Login succeeds but the save button returns 404"
→ curationJsonExportUrl is still pointing at mp.ex.nii.ac.jp. A miss from §3. Re-grep with grep -r curationJsonExportUrl iiif-curation-* to catch leftovers.
"Save works, but Activity Stream returns 404"
→ This mirror uses the Workers version as mainline, which does not implement upstream's Activity Stream pagination. Only the display collection endpoint (/as/collection.json) is provided. The Viewer's normal workflow is unaffected; but if you have a separate Activity Stream consumer that paginates, switch the backend to the PA upstream version.
Rollback
When the upstream URL returns, the original configuration can be restored in three steps:
-
Roll back the client-side URLs
find iiif-curation-* -name 'index.js' -exec \ sed -i '' -E "s|<jsonkeeper-base>|https://mp.ex.nii.ac.jp/api/curation/json|g" {} + find iiif-curation-manager -name 'index.js' -exec \ sed -i '' -E "s|\.\./iiif-curation-(viewer\|editor\|player\|board)/|http://codh.rois.ac.jp/software/iiif-curation-\1/demo/|g" {} + -
Revert
authFirebase.js'sfirebaseConfigback to the original (codh-81041) — once the upstream public branch is reachable again,git checkouteachauthFirebase.js. KeepingsignInFlow: 'popup'is optional (it doesn't change upstream behavior). -
Tear down the JSONkeeper servers — Workers version:
wrangler delete jsonkeeper; PA version: one Web App delete via the PA API (details in the other post). Leave Firebase projectkunshujo-calone if it's shared with other use cases; delete it from Console if it was project-specific.
Security cleanup
- The Firebase Web SDK
apiKeyis a client-visible identifier by design, but if you want to discourage unauthorized embedding, App Check is the primary tool to reach for. Alternatively you can apply an HTTP referrer restriction to the API key via GCP Console > API & Services > Credentials, but with some Firebase JS SDK fetch implementations the Referer header is not sent, which breaks Auth calls with a 403 (see firebase-js-sdk#5657). This mirror is on the old v5 SDK and is unlikely to be directly affected, but if you do apply referrer restrictions, always validate againstidentitytoolkit.googleapis.comcalls in staging before rolling to production. - Firebase Authentication's usage logs can be audited via Firebase Console > Authentication > Users. Whether to delete leftover user records after retirement depends on your retention policy.
- On the Workers + D1 side, you can hit D1 directly with SQL (
wrangler d1 execute) to delete fromdocuments. At teardown, drop the whole table and thenwrangler deletefor a fully-clean state.
Closing thoughts
This post is the record of walking back the earlier "won't work" claim and actually wiring up auth + storage. Three points stand out in retrospect:
- How far to push a temporary mirror is a balance between expected recovery date and operational burden. As the upstream outage stretches with no announced reopen, the use case shifts from read-only to needing writes, and self-hosting the server-side becomes the more realistic option.
- FirebaseUI 3.x's
signInFlowdefaults to redirect. Under a GitHub Pages subpath plus query parameters, an explicitpopupis effectively required. The fix is a one-line difference, but I only caught it by comparing Viewer against the other tools. - The client-side substitution boils down to a single class of URL (
curationJsonExportUrlplus the Firebase Web config). Running 360-line Workers code and a parallel PA HTTP-API deployment just for that one switch may look excessive, but the easy rollback and clean teardown make the split well worth it.
Wishing for a swift restoration of the upstream service, I'm keeping the codh-mirror layer in a state where it can be peeled off in a single command once the upstream is back.