← Back to Gists

The moderaton changes made to DevPlace

📝 Markdown Rendered
retoor
retoor · Level 50563 ·

This is all for apple verification to apply our platform (@Lensflare iOS app) to the app store.

DevPlace: gap analysis against the Apple App Store requirement register

Author: retoor retoor@molodetz.nl

Stage two of apple.md. Input is the requirement register in applecomp.md ยง8. Output is the exhaustive list of changes DevPlace needs to make an iOS client of this platform publishable. The implementation design is appleimpl.md.

Every verdict below is backed by a file reference read during the traversal. No verdict is inferred from documentation; documentation was only used to locate code.


1. Method

The traversal covered, recursively:

  • devplacepy/routers/ - every router file and package, for the full endpoint surface.
  • devplacepy/models.py, devplacepy/schemas/ - every input form and output schema.
  • devplacepy/database/ - schema.py (column ensure blocks), soft_delete.py (SOFT_DELETE_TABLES), the batch helpers.
  • devplacepy/templates/ - every template that renders a content action bar, the admin shell, the footer, the docs registry.
  • devplacepy/services/ - audit, devii, openai_gateway, containers, messaging, game, quiz, bot, news.
  • devplacepy/content.py, devplacepy/responses.py, devplacepy/templating.py - the shared predicates and response choke points.
  • devplacepy/main.py - middleware stack and router mounts.

2. Inventory: every user-generated-content surface

Requirement R5 (report on every UGC surface) and R4 (filter on every UGC surface) are only satisfiable against a complete list. This is that list, derived from SOFT_DELETE_TABLES in devplacepy/database/soft_delete.py:7 cross-checked against the routers that write each table.

# Surface Table Write entrypoint Visible to
S1 Posts posts routers/posts.py via content.create_content_item Public
S2 Comments (polymorphic: post, project, gist, news) comments routers/comments.py via content.create_comment_record Public
S3 Gists gists routers/gists.py Public
S4 Projects (title, description, devlog) projects routers/projects/ Public or private
S5 Project files (arbitrary text/binary) project_files routers/projects/files/ Public or private
S6 News submissions news routers/news.py, services/news/ Public
S7 Uploaded media / attachments attachments routers/uploads.py, attachments.py Follows parent
S8 Direct messages messaging store routers/messages.py:245 send_message + /messages/ws Two parties
S9 Quizzes, questions, options quizzes, quiz_questions, quiz_options routers/quizzes/ Public
S10 Poll questions and options polls, poll_options routers/polls.py Public
S11 Awards (user-issued citations) awards routers/awards.py Public
S12 Profile fields: bio, location, git link, website users models.py:408 ProfileForm Public
S13 Username and avatar seed users routers/auth/signup.py, routers/profile/avatar.py Public
S14 Issue tickets and issue comments issue_tickets (Gitea-backed) routers/issues/ Public
S15 Devii assistant output (chatbot under guideline 4.7) devii_conversations services/devii/ Owner, and anything it publishes
S16 User-authored virtual tools and lessons devii_virtual_tools, devii_lessons services/devii/ Owner
S17 Per-user custom CSS/JS user_customizations services/devii/customization/ Owner's own browser only
S18 Container workspaces and anything they serve instances, tunnels services/containers/, routers/proxy.py (/p/{slug}) Public via ingress
S19 DeepSearch sessions and exports deepsearch_sessions, deepsearch_messages services/jobs/deepsearch/ Owner
S20 AI usage analysis reports isslop_analyses services/jobs/isslop/ Owner

Twenty distinct surfaces. Sixteen of them (S1-S14, S18, and S15's published output) are visible to at least one other person and therefore fall inside guideline 1.2's scope. This breadth is the single defining constraint of the implementation: any design that requires per-surface bespoke code will be incomplete on the day it ships and will decay afterwards.


3. Inventory: what already exists and can be reused

Capability Where Fitness for the requirement
Block and mute routers/relations.py (/block/{username}, /mute/{username}, and the unblock/unmute inverses), user_relations table, _drop_blocked in database/comments.py Satisfies R9 functionally. Reachability from content is a gap (see G9).
Soft delete across the board database/soft_delete.py, SOFT_DELETE_TABLES (44 tables) Every content removal is already reversible and auditable, which is exactly what P2 and DSA statements of reasons need.
Admin Trash routers/admin/trash.py, /admin/trash, restore/purge by event Moderator undo path already exists.
Append-only audit log services/audit/, 288 keys in events.md, /admin/audit-log The evidence substrate for P1, P2 and the 24-hour SLA proof.
Account deactivation users.is_active, admin toggle at routers/admin/users.py:179, devrant DELETE /api/users/me at routers/devrant/auth.py:189 Not account deletion. Apple explicitly rejects deactivation-only. See G12.
Admin seniority guard _is_senior_admin in routers/admin/users.py Reusable for moderator-action authorization.
Workspace moderation flags services/containers/workspace/flags.py - raise_flag, clear_flag, set_status, list_flags, statuses open/acknowledged/resolved/dismissed, severities info/warn/critical, soft-deletable workspace_flags table The closest existing analogue to a report queue. It is instance-scoped, machine-raised and admin-resolved. Its state machine, severity ladder and audit shape are the correct precedent to generalise from.
Per-user AI opt-in users.ai_correction_enabled (default 0) and users.ai_modifier_enabled (default 1), routers/profile/ai_correction.py, routers/profile/ai_modifier.py Establishes the pattern for a consent flag on the user row. Partially serves R15 but is feature-scoped, not consent-scoped, and one of the two defaults to on.
Notification preferences notification_preferences table, NOTIFICATION_TYPES ร— NOTIFICATION_CHANNELS, routers/profile/notifications.py Push is already per-type, per-channel and user-controlled - R17 is close to satisfied.
Polymorphic target pattern (target_type, target_uid) on comments, votes, reactions, bookmarks; resolve_target_redirect() in comments.py; database/ranking.py VOTABLE_TARGETS/STAR_TARGETS; database/content.py resolve_object_url The load-bearing reuse. A report is structurally identical to a vote: one row keyed on (target_type, target_uid) plus an actor. Reporting must be built on this exact pattern, not beside it.
Devii action catalog services/devii/actions/catalog/, CONFIRM_REQUIRED in dispatcher.py Every new route gets its agent face here, per the root CLAUDE.md four-faces rule.
Docs prose registry routers/docs/pages.py DOCS_PAGES, e.g. the existing block-and-mute and admin-only media-moderation pages The publication channel for terms, community guidelines and privacy policy, with role gating already implemented.
Site settings site_settings, get_setting/get_int_setting, /admin/settings Where the moderation SLA, minimum age and filter aggressiveness belong - live-editable, no restart.
AI gateway services/openai_gateway/, /openai/v1/*, per-user cost attribution Single choke point through which every third-party AI call passes. R15's consent gate has exactly one correct insertion point because of this.

4. The gap register

Verdicts: MISSING (does not exist), PARTIAL (exists but does not meet the requirement), PRESENT (meets the requirement), N/A (not triggered).

4.1 Mandatory requirements

Req Requirement Verdict Evidence Change needed
R1 Terms of service / EULA stating zero tolerance for objectionable content and abusive users MISSING No terms, EULA, or legal page anywhere. Grep for terms/eula/privacy polic across templates/ and routers/ returns only four unrelated docs pages (bots and Code Farm prose). _footer_links.html links Docs, Swagger, OpenAPI, Issue Report only. Author the document; publish it as a first-class page; link it from the footer, the signup form and account settings.
R2 Recorded affirmative acceptance at account creation, re-acceptance on material change MISSING routers/auth/signup.py collects username, email, password, confirm only. models.py:51 SignupForm has four fields. No acceptance column on users (database/schema.py:1823ff enumerates every ensured column; none is terms-related). Add a required acceptance control to signup; persist the accepted document version and timestamp; force re-acceptance when the version changes.
R3 Community guidelines enumerating prohibited content per 1.1.1-1.1.7 MISSING No such document. Author and publish; reference from the terms and from every report dialog.
R4 Automated filtering of objectionable material at post time on every surface MISSING No content filter exists. The only blocklist occurrences in the codebase are the bot quality gate (TRIVIAL_GIST_TERMS, GENERIC_COMMENT_PHRASES) documented in templates/docs/bots-content.html:38 - these judge whether generated content is interesting, not whether user content is objectionable, and they run only on bot output. Introduce a filter that runs on every user-authored text at the single creation choke point, with an admin-tunable severity, that can block, hold for review, or flag.
R5 Report mechanism on every UGC surface MISSING No report route, table, template, schema, or Devii action exists. routers/relations.py provides block/mute only. services/containers/workspace/flags.py flags workspaces, machine-raised, and is not reachable by a member for content. Build a polymorphic report facility covering all sixteen externally-visible surfaces in ยง2.
R6 Moderation queue with triage, decision and enforcement MISSING /admin sidebar (templates/admin_base.html:11-59) has Users, News, Media, Trash, Services, Gateway, Containers, Workspaces, Devii tasks, Bots, Game, AI usage, Statistics, Audit log, Backups, Notifications, Settings. There is no moderation section. /admin/media handles only already soft-deleted media. Add a moderation queue as a first-class admin section, in the established admin_section pattern.
R7 Published 24-hour response commitment, and a mechanism that evidences it MISSING No SLA is published or measured. Publish the commitment in the terms and the report confirmation; measure age-of-oldest-open-report; surface it to admins and alert on breach.
R8 Ejection of offending users as a first-class enforcement action PARTIAL users.is_active toggled at routers/admin/users.py:179. It is a bare on/off with no reason, no duration, no linkage to a report, and no notice to the user. routers/devrant/auth.py:189 sets the same flag as "delete account". Promote to a suspension/ban action carrying reason, scope, duration and a link to the report that caused it, and generating a statement of reasons (P3).
R9 Block abusive users PARTIAL Fully implemented at routers/relations.py:87-104 with enforcement in database/comments.py _drop_blocked. The gap is discoverability: the action is only reachable from a profile page. templates/_post_card.html:32ff and templates/_comment.html:27ff action bars offer Reply/Edit/Delete/React/Share and no Block. Surface block from the content action bar alongside report; verify DM enforcement.
R10 Published contact information reachable inside the app PARTIAL _footer_links.html links /issues ("Issue Report"), which is a Gitea-backed bug tracker requiring an account, not a contact route. No postal address, no email, no phone. Publish a contact page carrying the DSA-mandated address, email and phone, linked from the footer and from settings.
R11 Privacy policy meeting 5.1.1(i)'s three content requirements, in-app MISSING No privacy policy exists. Author to the three-point spec; publish; link in-app and supply the URL to App Store Connect.
R12 In-app account deletion of the account record and associated personal data MISSING The only account-removal path in the product is DELETE /api/users/me (routers/devrant/auth.py:189) which sets is_active = False and revokes tokens - deactivation, which Apple's account-deletion support page names as explicitly insufficient. There is no route under /profile or /auth for deletion. Build a real, self-service, reauthenticated deletion that removes the account record and the associated personal data, discoverable in account settings.
R13 Declared-age gate at account creation, plus age-based access restriction MISSING No birthdate, age or date-of-birth field exists anywhere: grep across models.py and database/ returns nothing. SignupForm has no age field. Collect a declared age at signup, store the derived age band (not the raw birthdate, per 5.1.4 data minimization), enforce a minimum age, and gate age-exceeding content on it.
R14 Content age labelling; mature content hidden by default MISSING No maturity flag on any content table. Add a maturity classification produced by the filter and settable by the author, and hide flagged content behind an explicit, age-gated opt-in.
R15 Explicit consent before user content reaches third-party AI, with disclosure PARTIAL Two per-feature toggles exist: users.ai_correction_enabled defaults to 0 (opt-in, compliant in shape) and users.ai_modifier_enabled defaults to 1 (opt-out - non-compliant), both at database/schema.py:1832-1841. Neither is framed as consent to third-party processing, neither names the provider, and neither covers the other AI paths: Devii (services/devii/), DeepSearch, SEO metadata generation, the AI usage analyzer, issue enhancement (services/gitea/enhance.py), news import, and bots. All of these route through /openai/v1/* (services/openai_gateway/). Introduce one explicit, named, versioned third-party-AI consent, defaulting to off, enforced at the gateway choke point, with the per-feature toggles kept as preferences subordinate to it.
R16 Easily accessible consent withdrawal MISSING No consent record exists, therefore nothing to withdraw. Consent record with a withdraw action in account settings, and a downstream effect that is real (processing stops).
R17 Push optional, marketing push opt-in, in-app opt-out PRESENT notification_preferences per type per channel (database/notifications.py), user-editable at routers/profile/notifications.py:17. Push registration is explicit at routers/push.py:32. Nothing in the app requires push to function. Verify no notification type is marketing-by-default; document the position for review notes.
R18 DMCA / IP notice-and-takedown channel MISSING None. Add an intellectual-property report reason to the report facility and a public notice-and-takedown page describing the counter-notice path.
R19 Demo account with pre-seeded content and complete review notes MISSING No provisioning path for a review account exists; registration_open (site_settings) can close signup entirely, which would leave a reviewer unable to create an account. Provide a stable demo account with visible content from other authors, so report and block can both be exercised. Write the review notes.
R20 Age-rating questionnaire answered from the real feature set BLOCKED BY R4/R5/R6/R13 The questionnaire asks whether the app has moderation systems, content filtering, reporting tools, blocking functionality and parental controls. Today four of five answers are "no". Answers become truthful only once R4, R5, R6 and R13 ship.
R21 App privacy details declared, including third-party AI processing BLOCKED BY R15 Nothing to declare against until the AI data flow is disclosed and consented. Declare Contact Info, User Content, Identifiers, Usage Data, Diagnostics, all Linked to You, none Used to Track You.
R22 EU trader status with address, phone, email MISSING (metadata) The same contact data R10 needs. Declare in App Store Connect; keep identical to the in-app contact page.
R23 IPv6-only reachability UNVERIFIED docker-compose.yml and nginx/nginx.conf.template were not confirmed to bind IPv6; uvicorn defaults are IPv4. Verify and, if needed, fix listen directives for the app, nginx, the WebSocket routes and the container ingress.
R24 Remote code execution positioned under the 2.5.2 educational exception PARTIAL Substantively compliant already: containers execute remotely (services/containers/), the browser IDE makes source completely viewable and editable (routers/projects/files/), and nothing alters the client binary. What is missing is the positioning: no documentation states this, and the review notes do not exist. Document the architecture for App Review; make the "code runs on our servers, never on your device" statement explicit in the product and the docs.
R25 Native client materially beyond a web wrapper OUT OF SCOPE (client) The iOS binary is not in this repository. The backend obligation is to expose every safety control as a JSON API so the native client can implement them natively rather than embedding web views. Covered by the four-faces rule.

4.2 Conditional requirements

Req Trigger present? Verdict Evidence
C1 Sign in with Apple or equivalent No N/A - must stay N/A Auth is exclusively DevPlace's own system: session cookie, X-API-KEY, Bearer, HTTP Basic, all resolved in get_current_user. routers/auth/ has no OAuth provider. Guideline 4.8 exempts apps that exclusively use their own account system. Adding any social login later immediately creates the Sign in with Apple obligation.
C2 IAP for digital goods No N/A - must stay N/A No payment processor anywhere: no Stripe, PayPal or checkout integration in the codebase. The Code Farm economy (services/game/) is earn-only; Stars and Era awards are not purchasable. AI quota is administered, not sold (devplace gateway quota set). Any future sale of coins, credits, quota or boosts inside the app triggers mandatory IAP.
C3 Loot-box odds disclosure No N/A Randomized game rewards are not purchasable with real money.
C4 Contest rules stating Apple is not a sponsor Borderline PARTIAL Code Farm Eras (devplace game era start/end) rank players and award Stars. As long as awards are cosmetic/status only and nothing of monetary value is given, 5.3 is not engaged. Any real prize engages it. Document the position.
C5 Index of offered software with universal links Yes MISSING Users can publish workspaces reachable via the ingress proxy /p/{slug} (routers/proxy.py) and other users can open them. Guideline 4.7.4 requires an index of that software with universal links. No such index exists.
C6 Ad reporting control No N/A No advertising anywhere in the codebase.
C7 App Tracking Transparency No N/A No cross-app or cross-site tracking; no third-party analytics SDK.
C8 Recording indicator and consent Yes MISSING Presence tracking (services/presence.py, last_seen), the live view relay (services/live_view_relay.py), Devii terminal sessions and the audit log all make a record of user activity. Guideline 2.5.14 requires explicit consent and a clear indication. Presence is currently silent and unconditional.
C9 Per-instance consent before sharing data with user software Yes MISSING Container workspaces and Devii virtual tools can receive platform data. 4.7.3 requires explicit user consent in each instance.

4.3 Posture requirements

Req Verdict Notes
P1 Compliance improvement plan on request MISSING Needs moderation throughput metrics, which need R6.
P2 Moderation decisions retained as an audit trail PARTIAL The audit log already records every state change and never raises into the caller (services/audit/). Moderation event keys do not yet exist in events.md.
P3 Statement of reasons to the actioned user MISSING Content is soft-deleted silently. The notification system (utils/notifications.py, create_notification) is the right delivery channel and already exists.
P4 Privacy labels kept in step with features MISSING Process obligation; needs a documented owner and a checklist entry in the feature workflow.
P5 Accurate "What's New" MISSING Process obligation on the client release.

5. The positioning conflict - the finding that outranks every table above

DevPlace currently markets itself as uncensored. This is not incidental copy; it is the product's stated identity in four places:

  • devplacepy/main.py:744 - the site description: "Share what you're building in an open, uncensored environment."
  • devplacepy/templates/base.html:9 - the default meta description, on every page.
  • devplacepy/templates/landing.html:120 - the landing hero paragraph, and at landing.html:134 a feature card headed "No Censorship".
  • devplacepy/database/schema.py:280 - the default site_tagline site setting, echoed in templates/admin_settings.html:24.

Guideline 1.2 requires a method for filtering objectionable material and makes removal of violating content the developer's explicit responsibility. An App Review reviewer who opens the landing page - which they will, because it is the Support/Marketing URL - reads a promise that the platform does not moderate. That single sentence is sufficient grounds for a 1.2 rejection regardless of how good the implementation is, because it is a public statement that the required controls are not exercised.

There is no technical fix for this. The positioning must change to something that is both true and compatible: the platform is open and uncensored in the sense that it does not editorialise developer opinion, while enforcing a floor of prohibited categories. The four sites above must be reworded in step, and the wording must match the terms of service and community guidelines exactly, because a mismatch between marketing and policy is itself a 2.3.1 problem.

This is flagged as a decision for the lord, not an assumption: it changes the product's public voice.


6. Consolidated change list

Grouped by the layer they land in, so the implementation document can sequence them. Nothing here is designed yet; this is scope, not solution.

6.1 Data layer

  1. A polymorphic reports store keyed on (target_type, target_uid), soft-deletable, with a state machine.
  2. Moderation decision records linked to reports, retained for the audit trail.
  3. Enforcement records: suspension/ban with reason, scope, duration, originating report.
  4. users columns: terms-acceptance version and timestamp; declared age band; third-party-AI consent version, timestamp and state; activity-recording consent.
  5. A maturity classification on content, produced by the filter and adjustable by the author.
  6. New site_settings keys: moderation SLA hours, minimum age, filter mode and thresholds, contact details, current policy document versions.
  7. New soft-delete table registrations and indexes for all of the above.

6.2 Server layer

  1. Report submission endpoints, polymorphic, member-authenticated, rate-limited.
  2. Report listing and decision endpoints for moderators, with the seniority guard.
  3. Enforcement endpoints (suspend, ban, lift) replacing the bare is_active toggle.
  4. Account deletion endpoint with reauthentication and a real data-removal cascade.
  5. Terms acceptance endpoint plus a gate that forces re-acceptance on version change.
  6. AI consent endpoints, and enforcement at the /openai/v1/* gateway choke point.
  7. Age declaration at signup, and an age predicate applied at every read of maturity-flagged content.
  8. The content filter, invoked at the single creation choke point that already exists in content.py.
  9. Public legal pages: terms, community guidelines, privacy policy, contact, notice-and-takedown.
  10. A published index of user-offered software with universal links (4.7.4).
  11. A presence/activity-recording consent and indicator (2.5.14).

6.3 View layer

  1. Report and Block controls in every content action bar - _post_card.html, _comment.html, and the detail templates for gists, projects, news, quizzes, media, messages and profiles.
  2. A report dialog reusing the existing modal system, with reasons mapped to the 1.1.x categories.
  3. Signup form: terms acceptance and age declaration.
  4. Account settings: delete account, withdraw consent, view acceptances.
  5. Admin moderation section in the admin_base.html sidebar with the queue, SLA indicator and decision UI.
  6. Footer links to terms, privacy, community guidelines and contact.
  7. Maturity interstitial for age-exceeding content, hidden by default.

6.4 Agent, docs, SEO layer

  1. Devii actions for report, moderation listing and decisions, with CONFIRM_REQUIRED on enforcement.
  2. docs_api entries for every new endpoint.
  3. DOCS_PAGES prose entries for the legal documents and a moderation page (admin-gated, like media-moderation).
  4. SEO: legal pages are public and indexable; moderation is noindex,nofollow.
  5. New audit event keys in events.md and category_for.

6.5 Positioning and process

  1. Reword the four "uncensored" sites so marketing, terms and behaviour agree.
  2. Review notes, demo account, age-rating questionnaire answers, privacy labels, trader status.
  3. IPv6 verification across app, nginx, WebSockets and container ingress.

Comments

No comments yet. Start the discussion.