Subprocessors
Every third party that receives anything, and what each one gets.
Every row below was verified by reading the code at the file and line named in the last column. Nothing here is from memory or from a vendor's marketing page.
Effective date: 4 September 2026 Last verified against the code: 4 September 2026 Last revised: 23 October 2026 — OpenAI is now also used to write practice problems one at a time and to solve each one a second time, without its answer, so its answer key is checked before a student sees it. It is sent the topic's course passages and a few passages of the course's own graded work; never what a student types as an answer, which is checked by our own code. No new provider. (18 October 2026: PostHog (product analytics) and Sentry (crash reports) added. Both receive a filtered copy of events and errors from our server only, under a keyed id rather than an account id, with no content of any kind; no SDK of either runs in a browser or the extension. Both can be switched off by a student for analytics, and both are refused in their European regions. Earlier the same day: OpenAI is now also sent a student's voice while they ask a course-chat question out loud, streamed through our server so the words appear as they speak (the recording is sent once instead only if that stream fails); none of it is kept by CourseLite, and the student's browser never connects to OpenAI. No new provider. Earlier the same day: OpenAI is now also sent the words of a Guided reading a student presses Listen on, one part at a time, to be spoken in an AI voice; the audio it returns is kept in Cloudflare R2 under the course. No new provider. (16 October 2026: a student who connects Canvas by sign-in or API token has our server make read-only requests to their own school's Canvas, which is not a subprocessor (section 3, below the table). OpenAI is now also sent a picture a student attaches to a course chat question, as it already was for a Guided tutor question; no new provider. 12 October 2026: Cloudflare R2 now also holds the original files of a course, so a student can open the real lecture; no new provider. 10 October 2026: OpenAI is shown the public addresses of candidate Commons pictures so it can choose one for a Guided Study reading. 5 October 2026: Crossref removed, and Google narrowed to reading, because the assignment writer that used them was removed that day. 30 September 2026: Wikimedia Commons added, and Cloudflare R2 for kept course pictures.))
This is the companion to privacy-policy.md. If a provider is not on this list, it does not receive your information — there is no analytics or error-tracking SDK in any browser or in the extension, no advertising network, and no data broker anywhere in CourseLite.
How to read the "role" column
- Processor — handles data on CourseLite's instruction, for CourseLite's purposes.
- Independent controller — decides for itself what to do with the data it receives, under its own policy. You have a direct relationship with them.
- Both — processor for the part we instruct, controller for its own operational and security purposes. Most infrastructure providers are here.
The role assignments below are our reading, not a legal conclusion, and have not been reviewed by a lawyer or confirmed against each provider's DPA. See the note at the end.
1. Providers that receive user data
| Provider | Purpose | Data categories it receives | Role | Privacy policy | Verified in code |
|---|---|---|---|---|---|
| Clerk (Clerk.com, Inc.) | Authentication, sessions, sign-up and sign-in UI. Also sets the sign-in cookie on courselite.org and, in the extension, is the only reader of it | Email address, password or social sign-in, session token, user id, and whatever Clerk's own bot check collects. Our server additionally asks Clerk for a user's email address and role | Independent controller for the account; processor for the id we read back | https://clerk.com/legal/privacy | functions/lib/auth.ts (the only place identity is established); src/main.tsx:33 (ClerkProvider); functions/lib/admin.ts:91-92 and functions/api/courses/ingest.ts:451 (createClerkClient, the only server-side Clerk lookups) |
| OpenAI (OpenAI, L.L.C. / OpenAI Ireland Ltd) | Chat answers, embeddings, reading a Guided Study article aloud in an AI voice (speech generation, since 2026-10-18), writing down a question a student asks out loud in a course chat (speech to text, since 2026-10-18), image transcription, flashcards, practice tests, Guided Study material and practice problems one at a time (writing one, and solving it a second time without its answer to check the key — since 2026-10-23), and — for a Guided Study session on a course too thin to teach from — a web search for public pages on the subject, run by OpenAI's own search tool | Course material text, retrieved passages, assignment briefs (for the assignment-page chat), the student's questions, the instructor's name, images nothing else could parse, pictures a student attaches to a Guided tutor or course chat question (redrawn in their browser first; never stored), and the course's own kept pictures when a Guided reading is about to show one or a practice test or Guided question is built on or shows one — sent with the material or the question so it can be written from and checked against the picture; for a Guided Study picture, the public Wikimedia Commons addresses of up to three candidate pictures and a short passage of the reading, so the provider can choose one or none (its servers fetch those pictures from Wikimedia). the words of a Guided reading a student presses Listen on, one part at a time as they listen and at most two parts ahead, built by our server from the student's own plan (the browser sends only ids) — OpenAI returns the spoken audio. a student's voice while they ask a course-chat question out loud (since 2026-10-18) — at most two minutes, streamed through our server as 24 kHz audio so the words come back while they speak, or the finished recording sent once if that stream fails; never stored by CourseLite, and relayed by our server so OpenAI never sees the student's address. No account identifier is sent, and no lecture recording or video is ever sent to OpenAI — the only audio it receives is a student's own spoken question | Processor | https://openai.com/policies/privacy-policy · API data: https://developers.openai.com/api/docs/guides/your-data | functions/lib/openai.ts:170 (the single JSON fetch, authorization + content-type only, no user: field; speech to text is the file's one multipart fetch, transcribeSpeech, with the same authorization header and the file, model and format only); functions/lib/vision.ts; speech: synthesizeSpeech in functions/lib/openai.ts, called only from functions/api/study/narration.ts; speech to text: transcribeSpeech in functions/lib/openai.ts (the recording) and openUpstream in functions/lib/livedictation.ts (the live stream, one WebSocket), both reached only from functions/api/chat/dictation.ts |
| AssemblyAI (AssemblyAI, Inc.) | Speech-to-text on lecture recordings the student explicitly uploads for transcription | That recording's audio or video, once, under a neutral filename rather than the student's own. No name, no account, no course, no document id, no CourseLite identifier of any kind. Nothing is sent unless the student presses transcribe on that specific recording | Processor | https://www.assemblyai.com/legal/privacy-policy | functions/lib/transcription/assemblyai.ts — POST /v2/upload, POST /v2/transcript, GET /v2/transcript/{id}, DELETE /v2/transcript/{id}. The DELETE removes their copy of both the transcript and the audio, and is why the bytes are uploaded rather than linked. Speaker labels, summarisation, sentiment, topic and safety classification are all explicitly not requested |
| Stripe (Stripe, Inc. / Stripe Payments Europe Ltd) | Subscription checkout, billing portal, payment processing | Email address, CourseLite user id (as client_reference_id), plan, and the card details you enter on Stripe's own page | Independent controller (payments); processor for the ids we set | https://stripe.com/privacy | functions/lib/stripe.ts:34 (API base), :196–203 (customer create, sends email), :272 (client_reference_id: input.userId), :133 (the single fetch) |
| Google LLC | Reading course files under drive.file, the file Picker, Google Fonts, and the YouTube Data API | The drive.file OAuth grant and requests to read the files you pick. CourseLite writes nothing to Google — the Docs and Slides writing it used to do was removed on 5 October 2026. Separately, your IP address and user-agent when the Inter webfont loads. Separately again, for a YouTube lecture in your course, the video's public id and nothing else — no name, no account, no course — sent from our server to ask whether that video has captions. We never download a YouTube transcript. Google does not tell CourseLite which account granted the access — drive.file carries no identity and no openid/userinfo.email scope is requested | Independent controller | https://policies.google.com/privacy | Scope: extension/manifest.template.json:29, extension/src/google/auth.ts:47, extension/src/google/token.ts:50, src/google/picker.ts:37. Fonts: index.html:33–36. YouTube: functions/lib/transcription/youtube.ts |
| Cloudflare, Inc. | Hosting the website and the API (Pages + Pages Functions), CDN, TLS, DDoS protection, the Turnstile bot check, private object storage (R2) holding a lecture recording while it is being transcribed, and a work queue (Queues) plus a background Worker that carry out that transcription; and — since 2026-09-30 — R2 also holds the course pictures kept for Guided Study, and — since 2026-10-12 — the original files of a course (PDF, pictures, Word, PowerPoint, Excel) kept so the student can open them, and — since 2026-10-18 — the audio of a Guided reading read aloud to a student | Every request: IP address, URL, headers, and request body. Turnstile additionally receives the challenge token and the IP for one verification. A recording a student uploads for transcription is held in R2 under a random name and deleted once its transcript is stored. A queue message contains record identifiers only — never transcript text, never media, never a credential and never a recording link. R2 also holds the pictures an import sent to be transcribed, and the original files kept so a student can open them, under keys that carry no name, title or account id. An original is served only to its owner, never shared, and never sent to OpenAI | Both | https://www.cloudflare.com/privacypolicy/ | Deploy target throughout functions/; wrangler.toml; public/_headers; Turnstile: functions/lib/turnstile.ts:75, :137, :144; R2 + Queues: functions/lib/transcription/store.ts, functions/lib/transcription/queue.ts, workers/transcription-consumer/; kept pictures: functions/lib/coursemedia.ts; originals: functions/lib/originals.ts, functions/api/sources/original.ts; narration audio: functions/lib/narration.ts |
| Neon, Inc. | The PostgreSQL database — where everything CourseLite stores actually lives | All stored data: course material, documents, embeddings, generated work, account rows, analytics, anti-abuse hashes, billing ids | Processor | https://neon.tech/privacy-policy | functions/lib/db.ts:90 (connect(), the single connection point); docs/DATABASE.md |
| PostHog, Inc. | Product analytics since 2026-10-18: which steps get used, how long they take, and where people get stuck — funnels from first visit to a course import, from an import to a study session, and from the plans to a subscription | From our server only, filtered: an event name from a fixed list, a route pattern (/course-work/:id, never a real id), how long the step took, whether it worked, the plan, whether it came from the website, the extension or the server, and short codes and counts from an allowlist (FORWARDED_KEYS). Keyed to u_ + an HMAC of the account id under ANALYTICS_ID_SALT, or a_ + an HMAC of a logged-out visitor's random session id — never the Clerk id, an email, a name, an IP address ($ip: null, $geoip_disable), a course, file or assignment name, assignment text, a question, an answer or a URL. Not sent at all for a student who switched analytics off or whose browser sends Global Privacy Control | Processor | https://posthog.com/privacy | functions/lib/thirdparty.ts — posthogEvent (the properties), sendEvents (the one POST {POSTHOG_HOST}/batch/); called only from TelemetryStore.record → forward in functions/lib/telemetry.ts, which first drops accounts with users.analytics_opt_out |
| Functional Software, Inc. (Sentry) | Crash reports since 2026-10-18, from the website, the extension and the server | From our server only: an error code, a message redacted by sanitizeMessage (emails, bearer tokens, keys and JWTs replaced first), the route pattern, the HTTP status, the request id, the release and environment, the platform, and — for a browser error — frame positions in our own /assets/*.js files only (src/analytics/frames.ts). The same keyed id as PostHog. No request body, no headers, no cookies, no breadcrumbs, no page content, no session replay. Crash reports are sent whether or not a student switched analytics off, and the Settings switch says so | Processor | https://sentry.io/privacy/ | functions/lib/thirdparty.ts — sentryEvent (the event), forwardError (the one envelope POST), captureServerException (called from functions/_middleware.ts); source maps are uploaded at build time by .github/workflows/deploy.yml and are never served |
| Wikimedia Foundation, Inc. | Openly licensed pictures in Guided Study readings, found only when the student left Include visuals on and their course had no picture of the idea | From our server: a short phrase naming a subject ("mitochondrion diagram"), written by the AI provider from the reading, and a user-agent naming CourseLite — no account identifier, no course name, no course text. From the student's browser, when a picture is shown: the IP address and user-agent any image request carries, with no referrer | Independent controller | https://foundation.wikimedia.org/wiki/Policy:Privacy_policy | functions/lib/guidedvisuals.ts (COMMONS_API, findOpenImage — called as fetchImpl(, so a search for fetch( does not find it); the browser half is src/guided/LessonVisual.tsx (referrerPolicy="no-referrer") |
Crossref was listed here until 5 October 2026, for resolving a DOI into a bibliography entry. That feature was part of the assignment writer, which was removed; nothing in CourseLite contacts Crossref now.
2. Providers involved in operating CourseLite, that do not receive user data
| Provider | Purpose | Why it is not a subprocessor | Verified in code |
|---|---|---|---|
| GitHub (Microsoft) | Source hosting and CI. Typecheck, build, deploy on push to main | The pipeline builds and deploys code. It holds no runtime secret and never touches production data — DATABASE_URL is a Cloudflare Pages runtime secret, never a CI secret | .github/workflows/ci.yml, .github/workflows/deploy.yml |
| npm registry | Build-time dependencies | Build machine only | package.json, package-lock.json |
3. Providers deliberately not used
Worth writing down, because the absence is the disclosure.
| Not used | Would have been for |
|---|---|
| Mixpanel, Amplitude, Google Analytics, Plausible, or any analytics SDK in a browser or the extension | Product analytics. Ours is two Postgres tables (usage_events, usage_daily) written from one file, plus the filtered server-side copy sent to PostHog |
| Bugsnag, Rollbar, Datadog, or any error-tracking SDK in a browser or the extension | Error tracking. Ours is one Postgres table (app_errors) plus Cloudflare's own logs, plus the filtered server-side copy sent to Sentry |
| Session replay, heatmaps, click or keystroke capture, from any provider | Nothing. PostHog's autocapture and recordings are never loaded, because no PostHog code runs in a browser |
| Any advertising network, ad SDK, or attribution provider | Nothing. There are no ads and no advertising code |
| Any data broker, enrichment or identity-resolution provider | Nothing |
| Any device-fingerprinting or bot-scoring vendor beyond Cloudflare Turnstile | Anti-abuse. Ours is five one-way hashes of relationships you chose |
| Any email marketing platform | Nothing. Transactional email only |
| Redis, KV, or any caching service | Nothing. There is no cross-request cache for user-scoped data, deliberately |
The outbound destinations, counted by reading rather than by grepping. There are seven, and two of them are ones a grep misses:
| Destination | Where the call is | How the host is chosen |
|---|---|---|
api.openai.com | functions/lib/openai.ts:170 | DEFAULT_BASE_URL in functions/lib/models.ts:36, overridable by OPENAI_BASE_URL |
api.stripe.com | functions/lib/stripe.ts:133 | STRIPE_API, a module constant (:34) |
challenges.cloudflare.com | functions/lib/turnstile.ts:144 | SITEVERIFY, a module constant (:75) |
commons.wikimedia.org | functions/lib/guidedvisuals.ts (findOpenImage) | COMMONS_API, a module constant. Called as fetchImpl(, so a search for fetch( misses it, and only ever with the model's short search phrase as a query parameter — never a URL from a response |
PostHog's ingestion address (us.i.posthog.com by default) | functions/lib/thirdparty.ts, sendEvents → post | POSTHOG_HOST, defaulting to the US region; an https origin only, and a European-region host is refused (outsideUnitedStates) |
Sentry's ingestion address (*.ingest.us.sentry.io) | functions/lib/thirdparty.ts, forwardError → post | Parsed out of SENTRY_DSN by parseDsn; an https DSN with a key and a numeric project only, and a European-region host is refused |
| Clerk's API | Inside @clerk/backend: verifyToken (functions/lib/auth.ts:20) fetches Clerk's signing keys, and createClerkClient(...).users.getUser (functions/lib/admin.ts:91-92, functions/api/courses/ingest.ts:451-452) reads a user | The SDK's own constant, derived from CLERK_SECRET_KEY. No fetch( appears in our source for it at all |
One destination is chosen by the student, and it is not a subprocessor. Since 16 October 2026 a student may connect Canvas by signing in with Canvas or pasting a Canvas API token. Our server then makes read-only requests to that student's own school's Canvas, at the address they gave (functions/lib/canvasconnect.ts, functions/api/canvas/proxy.ts): course reads from one fixed allowlist (src/ingest/canvas/routes.ts), plus the sign-in's own token exchange, renewal and cancellation. It is not a provider CourseLite has chosen or a contract it holds: it is the student's school's system, receiving the student's own permission back. The address is checked before anything is sent (src/canvas/origin.ts): https only, no port, no IP address, no internal or private name, never CourseLite's own domain — and a redirect is followed only by hand, never with the permission attached once it leaves that Canvas.
No other user-supplied URL is fetched server-side, on any of these paths.
4. Where the data physically sits
| Where | |
|---|---|
| Website and API | Cloudflare's global edge network |
| Database | Neon, in the United States (AWS us-west-2, Oregon) |
| Kept course pictures | Cloudflare R2 (since 2026-09-30) |
| Original course files, kept so a student can open them | Cloudflare R2 (since 2026-10-12) |
| Audio of a Guided reading read aloud to a student | Cloudflare R2 (since 2026-10-18), under its course; made by OpenAI from the reading's words |
| Model inference | OpenAI's infrastructure |
| Product analytics | PostHog, United States region (since 2026-10-18) |
| Crash reports | Sentry, United States region (since 2026-10-18) |
| Authentication | Clerk's infrastructure |
| Payments | Stripe's infrastructure |
Assume the United States unless a region is filled in above, and see section 15 of the privacy policy.
5. Changes to this list
If we add a provider that receives user data, we will add it here and update the privacy policy before it starts receiving anything.
6. What is not verified
Kept separate from the table on purpose, so nobody mistakes one for the other.
- The role column is our reading. It has not been checked against any provider's data processing agreement, and none has been signed or reviewed.
- No DPA is in place with any provider named above. Several offer one. If CourseLite ever serves users in the EEA or the UK, a signed DPA with Clerk, Neon, OpenAI, Stripe, Cloudflare, PostHog and Sentry is a prerequisite, not a nicety.
- No international transfer mechanism has been confirmed for any provider.
- Neon's point-in-time restore window is Neon's to set, not ours. The region is stated above; the length of the backup window is a schedule Neon operates and we do not control, which is why neither this page nor the privacy policy quotes you a number of days for it.
- Sub-processors of our providers — the people Clerk, Neon, OpenAI, Stripe and Cloudflare use in turn — are listed on their own pages and are not reproduced here. Each provider maintains its own list.
- The legal entity names above are the ones the providers publish. Confirm which entity actually contracts with CourseLite before relying on them in a contract.
- Sub-processor lists change without telling us. This page is a snapshot of what the code does on the date at the top, not a subscription to each provider's own list.
7. Contact
| A question about anything on this page | support@courselite.org |
| Notice that a provider's details here are out of date | support@courselite.org |
| Privacy questions generally | support@courselite.org — see the Privacy Policy |
CourseLite is an online service with no public office, so there is no postal address to write to. Email reaches a person.
If you are a school or a university evaluating CourseLite, this page and the Privacy Policy are the two documents to read together, and the honest summary is the one in section 6: no data processing agreement has been signed with any provider named above, and no international transfer mechanism has been confirmed. If your institution requires either, CourseLite does not meet that requirement today, and we would rather you learn it here than after a procurement review.
CourseLite is an independent product and is not affiliated with, endorsed by, or connected to Instructure, Canvas, Blackboard, Moodle, Google, OpenAI, or any university, college or school.