The moderaton changes made to DevPlace
📝 Markdown RenderedThis 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 defaultmeta description, on every page.devplacepy/templates/landing.html:120- the landing hero paragraph, and atlanding.html:134a feature card headed "No Censorship".devplacepy/database/schema.py:280- the defaultsite_taglinesite setting, echoed intemplates/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
- A polymorphic reports store keyed on
(target_type, target_uid), soft-deletable, with a state machine. - Moderation decision records linked to reports, retained for the audit trail.
- Enforcement records: suspension/ban with reason, scope, duration, originating report.
userscolumns: terms-acceptance version and timestamp; declared age band; third-party-AI consent version, timestamp and state; activity-recording consent.- A maturity classification on content, produced by the filter and adjustable by the author.
- New
site_settingskeys: moderation SLA hours, minimum age, filter mode and thresholds, contact details, current policy document versions. - New soft-delete table registrations and indexes for all of the above.
6.2 Server layer
- Report submission endpoints, polymorphic, member-authenticated, rate-limited.
- Report listing and decision endpoints for moderators, with the seniority guard.
- Enforcement endpoints (suspend, ban, lift) replacing the bare
is_activetoggle. - Account deletion endpoint with reauthentication and a real data-removal cascade.
- Terms acceptance endpoint plus a gate that forces re-acceptance on version change.
- AI consent endpoints, and enforcement at the
/openai/v1/*gateway choke point. - Age declaration at signup, and an age predicate applied at every read of maturity-flagged content.
- The content filter, invoked at the single creation choke point that already exists in
content.py. - Public legal pages: terms, community guidelines, privacy policy, contact, notice-and-takedown.
- A published index of user-offered software with universal links (4.7.4).
- A presence/activity-recording consent and indicator (2.5.14).
6.3 View layer
- 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. - A report dialog reusing the existing modal system, with reasons mapped to the 1.1.x categories.
- Signup form: terms acceptance and age declaration.
- Account settings: delete account, withdraw consent, view acceptances.
- Admin moderation section in the
admin_base.htmlsidebar with the queue, SLA indicator and decision UI. - Footer links to terms, privacy, community guidelines and contact.
- Maturity interstitial for age-exceeding content, hidden by default.
6.4 Agent, docs, SEO layer
- Devii actions for report, moderation listing and decisions, with
CONFIRM_REQUIREDon enforcement. docs_apientries for every new endpoint.DOCS_PAGESprose entries for the legal documents and a moderation page (admin-gated, likemedia-moderation).- SEO: legal pages are public and indexable; moderation is
noindex,nofollow. - New audit event keys in
events.mdandcategory_for.
6.5 Positioning and process
- Reword the four "uncensored" sites so marketing, terms and behaviour agree.
- Review notes, demo account, age-rating questionnaire answers, privacy labels, trader status.
- IPv6 verification across app, nginx, WebSockets and container ingress.
Comments
No comments yet. Start the discussion.