This project is a small Cloudflare Workers application for managing and serving short stories. The worker exposes a REST API backed by a D1 database and an R2 bucket for story media and serves a React based frontend from the public directory.
Stories may be scheduled by specifying a future date when submitting. Scheduled stories are hidden from the main viewer until their publish date but remain accessible through the manage interface or by direct link.
- Public story viewer with scheduled publishing, previous/next navigation, and optional image or video playback.
- Editor-only submit, edit, and manage pages with search, date filtering, calendar highlights, and R2-backed media uploads.
- Google OAuth access control with
readerandeditorroles, plus optional public viewing throughPUBLIC_VIEW. - Edge-cached media delivery for images and videos, including byte-range and conditional request support.
- Header-authenticated automation APIs for calendar lookup, media upload, and story create/update workflows.
- Codex story generation workspace at
/generate-story, launched from/manage, with Sprite-backed jobs, reference images, live logs, feedback messages, cancellation, and review links.
Install the dependencies once using npm:
npm installStart a local development server using Wrangler:
wrangler devThe application will be available at http://localhost:8787.
Create a D1 database with this structure. The matching schema files are in
db/stories_table.sql and db/allowed_accounts_table.sql.
CREATE TABLE stories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL,
date DATE NOT NULL,
image_url TEXT,
video_url TEXT,
created DATETIME DEFAULT CURRENT_TIMESTAMP,
updated DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_stories_date ON stories (date DESC);
CREATE INDEX idx_stories_title_content ON stories(title, content);
CREATE INDEX idx_stories_id ON stories(id);
CREATE TABLE allowed_accounts (
email TEXT PRIMARY KEY,
role TEXT NOT NULL DEFAULT 'editor' CHECK (role IN ('reader', 'editor'))
);
Agentic story generation from /manage and /generate-story also requires the
tables in db/story_agent_tables.sql.
Deploy the worker to Cloudflare with:
wrangler deployThis uses the configuration defined in wrangler.jsonc which binds:
DB– a D1 database namedbedtime-storiesIMAGES– an R2 bucket namedstory-imagesASSETS– static files from thepublicdirectory
Ensure these resources exist in your Cloudflare account before deploying.
For an existing deployment created from the older schemas, apply the one-time security migration before deploying this version. It revokes legacy agent callback tokens, adds callback expiry, removes invalid account roles, and adds write-time role validation:
npx wrangler d1 execute bedtime-stories --remote --file db/security_hardening.sqlAccess to the worker is protected using Google OAuth. Permitted accounts are
listed in the allowed_accounts table of the D1 database along with a role
value of either reader or editor. Readers can only view stories while
editors may add, modify or delete them. Authentication fails closed: an empty
table, a missing account, or an unknown role grants no access. Seed the initial
owner during deployment, substituting the actual email address:
npx wrangler d1 execute bedtime-stories --remote \
--command "INSERT INTO allowed_accounts (email, role) VALUES ('owner@example.com', 'editor')"The Google OAuth client secret should be stored as GOOGLE_CLIENT_SECRET; the
non-secret client ID is configured as GOOGLE_CLIENT_ID. Google ID tokens are
verified locally against Google's signed JWKS and required claims. Application
sessions expire after seven days.
The bundled generate-story skill and Sprite runner use a header-authenticated
automation API. Set STORY_API_TOKEN as a Worker secret and send it in
X-Story-Token.
GET /api/stories/calendar?start=YYYY-MM-DD&end=YYYY-MM-DDreturns days with scheduled stories for date selection.POST /api/mediaacceptsmultipart/form-datawith afilefield, stores an image or video in R2, and returns the media key.POST /api/storiesaccepts JSON withtitle,content(Markdown), optionaldate, and optionalimage_url/video_urlR2 keys.PUT /api/stories/:idaccepts partial JSON updates fortitle,content,date,image_url, andvideo_url.
The /manage page links to /generate-story, where editors can launch
story-generation jobs in a preconfigured Sprite. Story ideas may include an
optional target date and up to three reference images selected from a file
picker or pasted into the prompt field. The page shows recent jobs, streams
runner logs with SSE, accepts feedback while the job is running, can cancel
active jobs, and links completed jobs back to the generated story.
Apply db/story_agent_tables.sql, keep the bedtime-stories Sprite updated
with the project and generate-story skill, and configure these Worker secrets:
SPRITES_API_TOKEN– Sprites API token used by the Worker to start commands.STORY_AGENT_ALLOWED_EMAILS– comma-separated editor emails allowed to run costly agent jobs.STORY_API_TOKEN– existing story automation token used by the Sprite runner.
Optional vars:
STORY_AGENT_SPRITE_NAME– defaults tobedtime-stories.STORY_AGENT_SPRITE_WORKDIR– defaults to/home/sprite/bedtimestories/main.STORY_AGENT_SPRITES_API_BASE– defaults tohttps://api.sprites.dev.STORY_AGENT_CODEX_HOME– defaults to/home/sprite/.codex-bedtimestories.STORY_API_USER_AGENT– browser-like user agent used by Sprite-side story API calls; override only if Cloudflare Browser Integrity Check starts blocking the default.
Codex auth inside the Sprite is project-specific. Before first use, create fresh
bedtimestories auth inside the bedtime-stories Sprite:
mkdir -p /home/sprite/.codex-bedtimestories
CODEX_HOME=/home/sprite/.codex-bedtimestories codex login --device-authDo not reuse or copy auth.json from another Sprite or project.
The runner creates a Sprite task hold while Codex is actively generating a story,
refreshes it during the run, and releases it when the job completes or fails.
Canceling a job also asks the Sprite to terminate the runner and delete that
task hold, making the Sprite eligible to pause when idle.
When reference images are included, the Worker stores them in R2, the runner
downloads them into /tmp on the Sprite, attaches them to Codex, and instructs
Codex to pass each path to generate-story as --ref-image.
If Sprite logs show Cloudflare Error 1010 (browser_signature_banned) on
/api/agent callbacks, deploy the current Worker code so the runner uses the
browser-like user agent. If the account-level security rule still blocks the
Sprite, disable Browser Integrity Check selectively for the authenticated
automation API paths rather than disabling story-agent authentication.
Agent admission is limited to one active job and five new jobs per account per hour. Each callback credential expires after one hour, runner execution stops after 50 minutes, provider polling and downloads are bounded, and terminal job records/reference images are deleted after seven days. Scheduled maintenance and authenticated job-list/create requests enforce expiry and retention.
allowed_accountsis fail-closed and accepts only the exact rolesreaderandeditor.- React 18.3.1 and every page script are self-hosted. HTML responses enforce
default-src 'self'; script-src 'self'and anti-framing directives. GET /update-cacherequiresCACHE_REFRESH_TOKEN(Bearer auth). Do not deploy without setting it.- Story-generation jobs require both an editor session and
STORY_AGENT_ALLOWED_EMAILS; leave the allowlist unset to disable this high-cost surface.
The worker exposes the following endpoints:
GET /– serves the story viewerGET /submit– serves a form to add a new storyGET /manage– serves a page to edit or delete storiesGET /generate-story– serves the Codex story generation workspaceGET /update-cache– warms recent media intocaches.defaultwith BearerCACHE_REFRESH_TOKENGET /images/:key– returns image or video media from theIMAGESbucket with long-term cachingGET /stories/list– returns all stories in JSONGET /stories/calendar?start=YYYY-MM-DD&end=YYYY-MM-DD– returns days with at least one story between the dates for calendar highlightingGET /stories– returns the most recent story not scheduled for the futureGET /stories/:id– returns a single storyPOST /stories– create a new story (multipart form data, fields:title,content,date, optionalimage, optionalvideo)PUT /stories/:id– update an existing story (multipart form data, fields:title,content,date, optionalimage, optionalvideo)DELETE /stories/:id– remove a storyGET /agent/jobs– list recent story-generation jobs for the authenticated editorPOST /agent/jobs– create an authenticated story-generation jobGET /agent/jobs/:id– return one story-generation jobGET /agent/jobs/:id/events– replay job events as an SSE streamPOST /agent/jobs/:id/messages– queue feedback for the running jobPOST /agent/jobs/:id/cancel– cancel an active story-generation jobPOST /api/media,POST /api/stories,PUT /api/stories/:id,GET /api/stories/calendar– token-authenticated automation endpoints
This project is licensed under the MIT License.
MIT License
Copyright (c) 2024 Bruce J. Hart
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished