<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: cursora</title>
    <description>The latest articles on DEV Community by cursora (@cursora).</description>
    <link>https://dev.to/cursora</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4023551%2F90eb85d7-eb7c-474a-a822-2e162a4dc6d7.webp</url>
      <title>DEV Community: cursora</title>
      <link>https://dev.to/cursora</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cursora"/>
    <language>en</language>
    <item>
      <title>Six bugs I found auditing code I wrote yesterday</title>
      <dc:creator>cursora</dc:creator>
      <pubDate>Fri, 31 Jul 2026 13:53:13 +0000</pubDate>
      <link>https://dev.to/cursora/six-bugs-i-found-auditing-code-i-wrote-yesterday-34jl</link>
      <guid>https://dev.to/cursora/six-bugs-i-found-auditing-code-i-wrote-yesterday-34jl</guid>
      <description>&lt;p&gt;Last week I shipped a lot: eleven new exercise types on Monday and Tuesday, a SQL exercise type running a throwaway Postgres per attempt on Wednesday. On Thursday I stopped adding things and spent the day attacking what I'd just built.&lt;/p&gt;

&lt;p&gt;Six bugs. Every one of them belongs to a class that generalizes, so here they are with names.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Gating the answer, but not the field that contains the answer
&lt;/h2&gt;

&lt;p&gt;Our auto-graded exercises have an author-supplied &lt;code&gt;explanation&lt;/code&gt; field. The tests for it contain strings like &lt;code&gt;"6*7=42"&lt;/code&gt;. It is, in practice, the answer written out in prose.&lt;/p&gt;

&lt;p&gt;The raw answer keys — &lt;code&gt;expectedAnswer&lt;/code&gt;, &lt;code&gt;correctCells&lt;/code&gt;, &lt;code&gt;correctOrder&lt;/code&gt; — were all correctly gated behind the author's &lt;code&gt;showCorrectAnswerOnFailure&lt;/code&gt; setting. In the same services. Written at the same time.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;explanation&lt;/code&gt; was returned unconditionally. In eight step types. One wrong attempt, and the response handed over the answer — no probing, no cleverness, and the author's explicit "don't reveal the answer on failure" setting silently ignored.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The class:&lt;/strong&gt; &lt;em&gt;secondary fields that contain the secret.&lt;/em&gt; You will remember to protect the thing that is obviously the secret. The risk is the human-readable field sitting next to it — the explanation, the hint, the error message, the debug payload, the log line — that contains the same information in a different shape. When you gate something, grep for every field that could restate it.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Correctness overlays are answer keys
&lt;/h2&gt;

&lt;p&gt;Same audit, a related one. Our matching exercise returned &lt;code&gt;pairResults&lt;/code&gt;: a map of &lt;code&gt;pairId → boolean&lt;/code&gt; telling the UI which pairs the student got right, so it can draw green and red.&lt;/p&gt;

&lt;p&gt;Returned unconditionally, on every attempt.&lt;/p&gt;

&lt;p&gt;That's not feedback, that's an oracle. Submit anything, read which pairs are &lt;code&gt;true&lt;/code&gt;, flip the ones that are &lt;code&gt;false&lt;/code&gt;, submit again. Two attempts to a perfect score without understanding the material.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The class:&lt;/strong&gt; &lt;em&gt;per-item correctness is equivalent to the key when items are independent.&lt;/em&gt; A single "you scored 3/8" is fine. "Which 3" is the answer key delivered in instalments. The same bug had already been fixed for the cell/position/indent variants in other step types — one shape was missed, and one is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. &lt;code&gt;@Body('field')&lt;/code&gt; silently skips validation
&lt;/h2&gt;

&lt;p&gt;This one is NestJS-specific and I'd bet real money it's in your codebase too.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The DTO exists. @MaxLength(8000) is right there on it.&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;studentSql&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;studentSql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ValidationPipe&lt;/code&gt; skips validation when the metatype is a primitive. &lt;code&gt;String&lt;/code&gt; is a primitive. So the DTO class sits in the repo, fully annotated, imported nowhere that matters, and &lt;strong&gt;not one of its constraints runs on this path.&lt;/strong&gt; Everything looks validated. Nothing is.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Fix: take the whole body, typed as the DTO.&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SubmitSqlChallengeDto&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once I knew the shape, I grepped for it — and found six more controllers doing the same thing, some via &lt;code&gt;@Body('field')&lt;/code&gt;, some via &lt;code&gt;data: any&lt;/code&gt;, some with an inline object literal as the parameter type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The class:&lt;/strong&gt; &lt;em&gt;validation that is present but not reachable.&lt;/em&gt; This is worse than no validation, because no validation is visible in review. A &lt;code&gt;@MaxLength&lt;/code&gt; sitting on an unreachable DTO reads as a control to everyone who looks at it.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. A session default is not a limit
&lt;/h2&gt;

&lt;p&gt;Our SQL sandbox runs student SQL as a restricted role, created per attempt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;sql_challenge_student&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;statement_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'5s'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a &lt;strong&gt;session default&lt;/strong&gt;. The student's own SQL can begin with &lt;code&gt;SET statement_timeout = 0;&lt;/code&gt; and it is gone. The mechanism I was relying on to stop runaway queries could be switched off by the queries it was supposed to stop.&lt;/p&gt;

&lt;p&gt;And the author-supplied &lt;code&gt;setup.sql&lt;/code&gt; and &lt;code&gt;verify.sql&lt;/code&gt;, which run as superuser, had no timeout at all. So a Pro-tier author could put &lt;code&gt;pg_sleep()&lt;/code&gt; in a setup script and quietly DoS the shared sandbox container pool — on every verify and every submit, for every student in the course.&lt;/p&gt;

&lt;p&gt;The fix moves the limit somewhere the workload can't reach it: every psql invocation wrapped in &lt;code&gt;timeout N&lt;/code&gt; at the process level. (Detail for anyone copying this: BusyBox &lt;code&gt;timeout&lt;/code&gt;, which is what you get in &lt;code&gt;postgres:16-alpine&lt;/code&gt;, exits &lt;strong&gt;143&lt;/strong&gt; on kill, not GNU's 124. If you only check for 124, your timeouts look like crashes.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The class:&lt;/strong&gt; &lt;em&gt;limits enforced inside the thing being limited.&lt;/em&gt; If the constrained party can execute code in the same context as the constraint, it isn't a constraint. Put the ceiling in the layer above — a process timeout, a cgroup, a supervisor.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Your CSV export executes code on someone else's machine
&lt;/h2&gt;

&lt;p&gt;Our gradebook export escaped CSV correctly by the usual definition: quotes doubled, fields with &lt;code&gt;"&lt;/code&gt;, &lt;code&gt;;&lt;/code&gt; or newlines wrapped. Textbook.&lt;/p&gt;

&lt;p&gt;It did nothing about a leading &lt;code&gt;=&lt;/code&gt;, &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt; or &lt;code&gt;@&lt;/code&gt;, which Excel and Google Sheets interpret as the start of a &lt;strong&gt;formula&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Students control their own names. A student sets their display name to a &lt;code&gt;HYPERLINK&lt;/code&gt; formula. The teacher exports the gradebook and opens it. The formula runs — on the teacher's machine, in the teacher's spreadsheet, with the teacher's data.&lt;/p&gt;

&lt;p&gt;The fix is a one-liner (prefix such fields with an apostrophe), and the interesting part is how the bug survived: the identical issue had &lt;strong&gt;already been fixed&lt;/strong&gt; in our attendance export, independently, by someone solving the same problem in a different file. Two implementations of "escape a CSV field", one hardened and one not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The class:&lt;/strong&gt; &lt;em&gt;escaping for the wrong consumer.&lt;/em&gt; We escaped for the CSV parser. The attacker's target was the spreadsheet application that opens the CSV afterwards. Ask who ultimately interprets your output, not what format you're emitting. (Same audit, same class: instructor-controlled strings interpolated unescaped into reminder emails sent to every enrolled student. HTML has a consumer too.)&lt;/p&gt;

&lt;h2&gt;
  
  
  6. The endpoint that forgot it was the paid tier
&lt;/h2&gt;

&lt;p&gt;The one I'd rather not write up, which is exactly why it's here.&lt;/p&gt;

&lt;p&gt;Our SQL exercise editor has a preview endpoint so authors can test their setup script while writing an exercise. &lt;code&gt;create()&lt;/code&gt; and &lt;code&gt;update()&lt;/code&gt; both enforce the Pro-tier plan gate. &lt;code&gt;preview()&lt;/code&gt; checked that you were logged in, and nothing else.&lt;/p&gt;

&lt;p&gt;What preview does is run the submitted &lt;code&gt;setupSql&lt;/code&gt; — as &lt;strong&gt;full Postgres superuser&lt;/strong&gt;, before the restricted student role exists, because that's what setting up an exercise requires. Postgres superuser includes &lt;code&gt;COPY ... TO/FROM PROGRAM&lt;/code&gt;: command execution inside the container.&lt;/p&gt;

&lt;p&gt;So: any logged-in account, including a free one, could reach superuser SQL execution in the sandbox. The plan gate on the two obvious endpoints created a completely convincing illusion of a guarded feature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The class:&lt;/strong&gt; &lt;em&gt;auxiliary endpoints inherit the feature's privileges but not its checks.&lt;/em&gt; Preview, dry-run, validate, test-connection, export-sample — the endpoints that exist so the main flow feels good, get written last, and get reviewed least. They usually reach exactly as deep as the endpoint they're previewing. Enumerate every route that touches a privileged capability, not every route that looks important.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually take from this
&lt;/h2&gt;

&lt;p&gt;Four of the six exist because a control was applied &lt;em&gt;somewhere&lt;/em&gt; and not everywhere: the answer key gated but not the explanation, the cell overlay gated but not the pair overlay, the plan gate on create/update but not preview, CSV formulas neutralized in one export but not the other. The failure mode isn't ignorance of the rule. It's incomplete application of a rule everyone involved already knew.&lt;/p&gt;

&lt;p&gt;Which suggests the audit question worth asking isn't "did we handle this?" It's &lt;strong&gt;"where else does this shape appear, and did we handle it there?"&lt;/strong&gt; Every one of these was found by taking a known control and grepping for every place it should exist.&lt;/p&gt;

&lt;p&gt;The other one: I audited this code the day after writing it, while I still remembered every decision — and still found six. The version of me that wrote it was certain it was fine.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>nestjs</category>
      <category>postgres</category>
    </item>
    <item>
      <title>A throwaway Postgres per attempt: running untrusted student SQL</title>
      <dc:creator>cursora</dc:creator>
      <pubDate>Thu, 30 Jul 2026 13:54:20 +0000</pubDate>
      <link>https://dev.to/cursora/a-throwaway-postgres-per-attempt-running-untrusted-student-sql-36gj</link>
      <guid>https://dev.to/cursora/a-throwaway-postgres-per-attempt-running-untrusted-student-sql-36gj</guid>
      <description>&lt;h2&gt;
  
  
  The requirement
&lt;/h2&gt;

&lt;p&gt;Students submit arbitrary SQL. It has to be safe. And it has to allow real DDL — because "design this schema and populate it" is most of what teaching databases consists of, and an exercise limited to &lt;code&gt;SELECT&lt;/code&gt; against a fixed fixture teaches a fraction of the subject.&lt;/p&gt;

&lt;p&gt;Two obvious designs, both rejected:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An in-process SQL engine in the backend.&lt;/strong&gt; Fast, no containers. Also means evaluating untrusted input inside the application process — a shape that has burned this codebase before, in a different feature, with a real RCE as the outcome. Not again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A shared teaching database with restricted grants.&lt;/strong&gt; Cheap. Also means shared mutable state between students, and shared mutable state plus thirty people learning &lt;code&gt;DELETE&lt;/code&gt; ends exactly one way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we built
&lt;/h2&gt;

&lt;p&gt;One throwaway &lt;code&gt;postgres:16-alpine&lt;/code&gt; container per attempt, executed through the same sandbox mechanism (epicbox) that already runs student Python, Java and Go.&lt;/p&gt;

&lt;p&gt;Per-attempt sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start Postgres&lt;/li&gt;
&lt;li&gt;Run the exercise author's &lt;code&gt;setup.sql&lt;/code&gt; as superuser&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CREATE&lt;/code&gt; a restricted role&lt;/li&gt;
&lt;li&gt;Run the student's SQL as that role&lt;/li&gt;
&lt;li&gt;Run the author's verification queries as superuser&lt;/li&gt;
&lt;li&gt;Emit JSON on stdout, &lt;code&gt;pg_ctl stop&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything the student did dies with the container.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making a database-per-attempt fast enough
&lt;/h2&gt;

&lt;p&gt;The naive version — start a Postgres container, &lt;code&gt;initdb&lt;/code&gt;, then use it — costs about ten seconds. That's the difference between a feature students use and one they avoid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;initdb&lt;/code&gt; is baked at image build time.&lt;/strong&gt; The image ships with an initialized data directory; the container starts an already-initialized cluster. This is the single change that made the approach viable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Durability is turned off.&lt;/strong&gt; &lt;code&gt;fsync&lt;/code&gt;, &lt;code&gt;synchronous_commit&lt;/code&gt;, &lt;code&gt;full_page_writes&lt;/code&gt; — all off. The entire state is discarded seconds later, so every guarantee Postgres offers about surviving a crash is pure cost here. This is one of the rare cases where turning off &lt;code&gt;fsync&lt;/code&gt; is not reckless but obviously correct.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unix socket only, no TCP.&lt;/strong&gt; The sandbox runs with networking disabled, so there's nothing to listen for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resource floors have to be raised.&lt;/strong&gt; Postgres forks helper processes, so limits tuned for a single-process interpreter fail immediately. We floor it at 384 MB and 64 PIDs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I'd defend hardest: don't filter the SQL
&lt;/h2&gt;

&lt;p&gt;We do not inspect the student's SQL. At all. No allowlist of statements, no regex for &lt;code&gt;DROP&lt;/code&gt;, no parsing.&lt;/p&gt;

&lt;p&gt;Instead, the student connects as a role created per attempt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;NOSUPERUSER&lt;/span&gt; &lt;span class="k"&gt;NOCREATEDB&lt;/span&gt; &lt;span class="n"&gt;NOCREATEROLE&lt;/span&gt; &lt;span class="n"&gt;NOREPLICATION&lt;/span&gt; &lt;span class="n"&gt;NOBYPASSRLS&lt;/span&gt;
&lt;span class="k"&gt;CONNECTION&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="n"&gt;statement_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="n"&gt;search_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a student tries &lt;code&gt;CREATE ROLE&lt;/code&gt; or &lt;code&gt;DROP DATABASE&lt;/code&gt;, &lt;strong&gt;PostgreSQL refuses them.&lt;/strong&gt; Not our filter — the engine's own privilege system, which is the only authoritative answer to "is this connection allowed to do that".&lt;/p&gt;

&lt;p&gt;Text-level filtering of a programming language is a losing game; there is always another spelling. That's true of JavaScript (this codebase learned it the hard way, with a bypassable regex allowlist over &lt;code&gt;vm.runInContext&lt;/code&gt;) and it's true of SQL. The database already contains a complete, battle-tested authorization system. Use that one.&lt;/p&gt;

&lt;p&gt;We verified it the boring way: wrote tests that attempt &lt;code&gt;CREATE ROLE&lt;/code&gt; and &lt;code&gt;DROP DATABASE&lt;/code&gt; and asserted the engine rejects them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two timeouts, because one doesn't cover both cases
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;statement_timeout = 5s&lt;/code&gt; on the student role kills a runaway student query. It does &lt;strong&gt;not&lt;/strong&gt; protect you from a runaway &lt;code&gt;setup.sql&lt;/code&gt;, because setup runs as superuser — and superuser can raise or ignore that setting.&lt;/p&gt;

&lt;p&gt;So there's a second, outer limit: the sandbox's hard wall-clock kill at ~20 seconds, which doesn't care who you are or what you set. Two failure modes, two mechanisms, one of them outside anything the workload can influence.&lt;/p&gt;

&lt;p&gt;If that sounds familiar, it's the same shape as enforcing sandbox session TTLs in two places — the inner mechanism does the graceful thing, the outer one exists because the inner one can be subverted or die.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test it on real containers or don't claim it works
&lt;/h2&gt;

&lt;p&gt;I want to be specific about this because it's the part that generalizes best.&lt;/p&gt;

&lt;p&gt;Seven scenarios were run against &lt;strong&gt;live containers&lt;/strong&gt;, not reasoned about: happy path, student syntax error, error in the author's setup, &lt;code&gt;CREATE ROLE&lt;/code&gt; rejected by the engine, &lt;code&gt;DROP DATABASE&lt;/code&gt; rejected by the engine, &lt;code&gt;statement_timeout&lt;/code&gt; killing a hung student query, and the outer limit killing a hung setup.&lt;/p&gt;

&lt;p&gt;That process found &lt;strong&gt;three bugs code review had not&lt;/strong&gt; — the most instructive being that the sandbox resets &lt;code&gt;/sandbox&lt;/code&gt; ownership to root on every file upload, which meant runtime-writable state had to move under a path baked into the image instead. There is no amount of reading the code that surfaces that.&lt;/p&gt;

&lt;p&gt;For anything where the failure mode is "untrusted code does something you didn't anticipate", the test that counts is the one that actually runs it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It's a container. This inherits whatever your container isolation is worth — for us, the same layer our other language executors run in, with no network.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;setup.sql&lt;/code&gt; runs as superuser. That's a deliberate trust boundary: exercise authors are trusted, students aren't. If your authors aren't trusted, this design doesn't transfer.&lt;/li&gt;
&lt;li&gt;A database engine per attempt costs real compute, which is why this sits on our paid tier. A shared instance with per-student schemas is dramatically cheaper; it's just a weaker isolation story.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>postgres</category>
      <category>sql</category>
      <category>docker</category>
      <category>security</category>
    </item>
    <item>
      <title>Deterministic seeds beat question banks for per-student problem variants</title>
      <dc:creator>cursora</dc:creator>
      <pubDate>Wed, 29 Jul 2026 18:10:14 +0000</pubDate>
      <link>https://dev.to/cursora/deterministic-seeds-beat-question-banks-for-per-student-problem-variants-2jh7</link>
      <guid>https://dev.to/cursora/deterministic-seeds-beat-question-banks-for-per-student-problem-variants-2jh7</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;You want every student to get a slightly different version of the same problem, so that comparing answers doesn't work.&lt;/p&gt;

&lt;p&gt;The obvious approach is a question bank: write twenty variants, assign each student one, store which one they got. It works. It also means someone has to write twenty variants of every question, and you now have a per-student assignment table that has to stay consistent forever.&lt;/p&gt;

&lt;p&gt;We did it differently, and the difference is one idea: &lt;strong&gt;generate the variant from a deterministic seed instead of storing it.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The design
&lt;/h2&gt;

&lt;p&gt;The author writes one template — a prompt with parameters, and a formula for the answer. At request time, the server derives a seed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;seed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computeRandomTaskSeed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lessonId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stepIndex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;resolvedPrompt&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolveRandomTaskProblem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawProblem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same three inputs, same seed, same variant — forever, with nothing persisted.&lt;/p&gt;

&lt;h2&gt;
  
  
  What falls out of it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Different students get different problems.&lt;/strong&gt; The whole point, and it comes from &lt;code&gt;userId&lt;/code&gt; being in the seed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The same student always gets the same problem.&lt;/strong&gt; This one matters more than it looks. The naive implementation — randomize per request — creates a re-roll attack: refresh until you get an instance with friendlier numbers. Students find this on day one. Because our seed contains no clock and no request nonce, refreshing produces a byte-identical question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No storage, no migrations, no drift.&lt;/strong&gt; There's no variant table to keep in sync, nothing to backfill when an author edits a template, and no possibility of "the question shown" and "the answer graded" disagreeing because a row went stale. The seed regenerates the same variant on demand, every time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Author edits are instant.&lt;/strong&gt; Change the template and every student's variant re-derives from it on the next page load. With a stored bank you'd be reconciling old assignments against a changed question.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that's a security decision, not a design decision
&lt;/h2&gt;

&lt;p&gt;The client must never receive the template, the parameter set, or the answer formula. Resolve on the server, send only the resolved prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;randomTaskProblem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canManageLessonAsAuthor&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;rawRandomTaskProblem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resolvedPrompt&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// author is editing it&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;resolvedPrompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                              &lt;span class="c1"&gt;// student sees only their own variant&lt;/span&gt;
      &lt;span class="nx"&gt;points&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;tolerance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;explanation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;showCorrectAnswerOnFailure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is genuinely tempting to ship the template and resolve client-side — it's less server work and the interaction feels snappier. It also puts the answer generator in the page source, which converts your randomization from an anti-copying measure into an inconvenience for students who don't open devtools.&lt;/p&gt;

&lt;p&gt;Note the shape of the branch: the &lt;em&gt;author&lt;/em&gt; gets the raw template because editing requires it. Role-based field selection at the boundary, not a single response shape that leaks to whoever asks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;Writing a good parameterized template is harder than writing a good fixed question. The author has to reason about parameter ranges: does every value in this range keep the problem meaningful, or does some combination produce a degenerate case — a divisor of one, a negative quantity where the story doesn't allow it, an answer that happens to be zero?&lt;/p&gt;

&lt;p&gt;A question bank is dumber and more predictable, and for some material that's the right trade. This isn't strictly better; it's better when the question has a parametric shape at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The generalizable bit
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Derive instead of store, when the derivation is deterministic and cheap.&lt;/strong&gt; Anything you store is something that can go stale, needs migration, and can disagree with reality. Anything you can regenerate from stable inputs can't.&lt;/p&gt;

&lt;p&gt;The trap is that the same property that makes it good — same inputs, same output — is exactly what you break the moment you add a timestamp or a random nonce to the seed "to make it more random". If you take one thing from this: whatever goes into that seed is the contract, and adding to it later is a breaking change for every student mid-course.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>edtech</category>
      <category>webdev</category>
      <category>security</category>
    </item>
    <item>
      <title>Four things that bit us building marketplace payouts on Stripe Connect</title>
      <dc:creator>cursora</dc:creator>
      <pubDate>Tue, 28 Jul 2026 13:12:34 +0000</pubDate>
      <link>https://dev.to/cursora/four-things-that-bit-us-building-marketplace-payouts-on-stripe-connect-1aka</link>
      <guid>https://dev.to/cursora/four-things-that-bit-us-building-marketplace-payouts-on-stripe-connect-1aka</guid>
      <description>&lt;h2&gt;
  
  
  The shape of the thing
&lt;/h2&gt;

&lt;p&gt;We added marketplace payouts to our education platform: course authors connect a Stripe account, learners buy courses, the platform takes a cut and the author gets the rest. Standard Stripe Connect territory.&lt;/p&gt;

&lt;p&gt;The overall design is a &lt;strong&gt;destination charge&lt;/strong&gt; — the platform stays merchant of record, takes an &lt;code&gt;application_fee_amount&lt;/code&gt;, and the remainder is transferred to the author's connected account:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;transfer_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;connectedAccountId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="nx"&gt;application_fee_amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;applicationFeeAmountGrosze&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That part took an afternoon. Here are the four things that didn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The fee base is the discounted amount, and you have to say so out loud
&lt;/h2&gt;

&lt;p&gt;Authors can issue promo codes. So for any given sale there are two numbers: the list price and what the buyer actually paid. Charging your platform fee against the list price when the author discounted the sale means the author eats the entire discount &lt;em&gt;plus&lt;/em&gt; a fee calculated on money nobody paid — and at a steep enough discount, the fee can exceed what they netted.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unitAmountGrosze&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;discountedAmountPln&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;applicationFeeAmountGrosze&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;discountedAmountPln&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;PLATFORM_FEE_RATE&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both derive from the same &lt;code&gt;discountedAmountPln&lt;/code&gt;. The rule worth writing into a comment (we did) is that there is exactly one authoritative amount per sale, and every downstream number is computed from it. Two amounts floating around means eventually one of them gets used in the wrong place, and that's a bug your authors report as theft.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. A 100%-off code cannot go through Checkout at all
&lt;/h2&gt;

&lt;p&gt;Stripe Checkout requires &lt;code&gt;unit_amount &amp;gt; 0&lt;/code&gt;. A full-discount promo code produces a total of zero, and there is no "free Checkout session" to create.&lt;/p&gt;

&lt;p&gt;You cannot fix this at the Stripe layer. It has to be a branch much earlier: if the discounted total is zero, skip payment entirely and grant the purchase directly, recording it with the same purchase record shape and a zero platform fee so downstream reporting doesn't have a hole in it.&lt;/p&gt;

&lt;p&gt;The lesson generalizes past Stripe: &lt;strong&gt;the free path is a different code path, not a special case of the paid one.&lt;/strong&gt; If you discover this after building the paid path, you will be tempted to fake a zero-amount payment to keep one flow. Don't — you'll be writing "if amount == 0 skip this" in five more places by the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. PaymentIntent cannot see the Session's metadata
&lt;/h2&gt;

&lt;p&gt;We reconcile purchases from webhooks. Success comes in on &lt;code&gt;checkout.session.completed&lt;/code&gt;, failure on &lt;code&gt;payment_intent.payment_failed&lt;/code&gt;. Naturally you attach your correlation IDs to the Checkout Session's &lt;code&gt;metadata&lt;/code&gt; and read them in the handler.&lt;/p&gt;

&lt;p&gt;That works for the success case and silently fails for the failure case: a PaymentIntent has its own metadata and does &lt;strong&gt;not&lt;/strong&gt; inherit the Session's. The failure webhook arrives carrying nothing you can join on, and you discover this when you actually need to debug a failed payment.&lt;/p&gt;

&lt;p&gt;The fix is to write the metadata twice — once on the session, once via &lt;code&gt;payment_intent_data.metadata&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                  &lt;span class="c1"&gt;// read by checkout.session.completed&lt;/span&gt;
&lt;span class="nx"&gt;payment_intent_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;         &lt;span class="c1"&gt;// read by payment_intent.payment_failed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not elegant. Necessary. Worth knowing before rather than after.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. &lt;code&gt;default: null&lt;/code&gt; on a sparse unique index is a trap
&lt;/h2&gt;

&lt;p&gt;Unrelated to payments, same release, too good not to include. Invite-only courses have an optional invite code with a &lt;strong&gt;sparse unique&lt;/strong&gt; index — unique when present, absent otherwise. The schema declared it as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Prop&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nx"&gt;inviteCode&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sparse&lt;/code&gt; skips documents where the field is &lt;strong&gt;missing&lt;/strong&gt;. It does not skip documents where the field is present and set to &lt;code&gt;null&lt;/code&gt;. With &lt;code&gt;default: null&lt;/code&gt;, every new course got an explicitly-stored &lt;code&gt;inviteCode: null&lt;/code&gt;, so the second course ever created collided with the first on &lt;code&gt;E11000 duplicate key&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The fix is to remove the default entirely and let the field be absent. Generally: &lt;strong&gt;&lt;code&gt;null&lt;/code&gt; is a value, &lt;code&gt;undefined&lt;/code&gt;/absent is not&lt;/strong&gt;, and every "optional unique field" bug I've seen comes from treating those as the same thing.&lt;/p&gt;




&lt;p&gt;None of these are exotic. All four cost real debugging time, and three of them only show up on the failure or edge path — the one you exercise last and in production first.&lt;/p&gt;

</description>
      <category>stripe</category>
      <category>payments</category>
      <category>nestjs</category>
      <category>webdev</category>
    </item>
    <item>
      <title>vm.runInContext is not a sandbox: how we replaced eval with a parser</title>
      <dc:creator>cursora</dc:creator>
      <pubDate>Mon, 27 Jul 2026 04:22:39 +0000</pubDate>
      <link>https://dev.to/cursora/vmrunincontext-is-not-a-sandbox-how-we-replaced-eval-with-a-parser-2o16</link>
      <guid>https://dev.to/cursora/vmrunincontext-is-not-a-sandbox-how-we-replaced-eval-with-a-parser-2o16</guid>
      <description>&lt;h2&gt;
  
  
  The feature
&lt;/h2&gt;

&lt;p&gt;One of our challenge types lets a learner write a MongoDB query and run it against a seeded dataset. The obvious implementation, and the one we shipped first, is the one you're probably picturing: take the query string, run it in Node's &lt;code&gt;vm&lt;/code&gt; module with a &lt;code&gt;db&lt;/code&gt; object in the context, return the result.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;runInContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;contextWithDb&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To keep it read-only, there was an allowlist: a regex over the query string checking that the methods called were things like &lt;code&gt;find&lt;/code&gt;, &lt;code&gt;aggregate&lt;/code&gt;, &lt;code&gt;countDocuments&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Both halves of that are wrong, and they're wrong in ways worth spelling out, because this exact shape shows up in a lot of "let users write a little expression" features.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the regex allowlist fails
&lt;/h2&gt;

&lt;p&gt;A regex like &lt;code&gt;\.(\w+)\(&lt;/code&gt; sees method calls written with dot notation. JavaScript does not require dot notation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({})[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;constructor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;constructor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;return process&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;.method(&lt;/code&gt; for the regex to object to, and &lt;code&gt;constructor.constructor&lt;/code&gt; is &lt;code&gt;Function&lt;/code&gt; — which builds a new function from a string. Once you can construct a function, the allowlist is decoration.&lt;/p&gt;

&lt;p&gt;Any allowlist that operates on the &lt;em&gt;text&lt;/em&gt; of a program rather than its &lt;em&gt;structure&lt;/em&gt; has this class of hole. There is always another way to spell the same operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;vm&lt;/code&gt; doesn't save you
&lt;/h2&gt;

&lt;p&gt;The natural response is "fine, but it's running in a &lt;code&gt;vm&lt;/code&gt; context, so it's contained." It isn't. Node's own documentation is explicit that the &lt;code&gt;vm&lt;/code&gt; module is not a security mechanism — it isolates the global object, not the process. Escapes via constructor chains on objects passed into the context are well documented, and in our case an object &lt;em&gt;had&lt;/em&gt; to be passed in: the &lt;code&gt;db&lt;/code&gt; handle the whole feature exists to expose.&lt;/p&gt;

&lt;p&gt;And that &lt;code&gt;db&lt;/code&gt; handle was a live connection to the production database. So the ceiling on this bug was not "a learner reads another collection". It was arbitrary code execution in the backend process, holding a production database connection.&lt;/p&gt;

&lt;p&gt;We found this in an internal audit in June, fixed it, and nothing indicates it was ever exercised in the wild — but "we found it before anyone else did" is luck, not architecture, and the architecture was the actual problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: parse, don't evaluate
&lt;/h2&gt;

&lt;p&gt;The rewrite executes no learner code at all. Not in a sandbox, not in a context, not anywhere.&lt;/p&gt;

&lt;p&gt;The query string is parsed into an AST with &lt;code&gt;acorn&lt;/code&gt;, and the AST is then &lt;em&gt;interpreted&lt;/em&gt; against a strict allowlist of node types: literals, arrays, objects, a couple of permitted constructor calls (&lt;code&gt;ObjectId(...)&lt;/code&gt;, &lt;code&gt;ISODate(...)&lt;/code&gt;), and a call chain matching &lt;code&gt;db.&amp;lt;collection&amp;gt;.&amp;lt;method&amp;gt;(...)&lt;/code&gt;. Anything else — a member expression that isn't in the shape we expect, an identifier we don't know, a function expression — is a rejection, not a fallback.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PRIMARY_READ_METHODS = find, findOne, aggregate, countDocuments, distinct
CHAIN_METHODS        = sort, limit, skip, project
WRITE_METHODS        = insertOne, updateOne, deleteMany, ... (author setup scripts only)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The structural difference that matters: &lt;code&gt;["constructor"]["constructor"]&lt;/code&gt; is not a mysterious edge case to this design. It's a &lt;code&gt;MemberExpression&lt;/code&gt; with a computed key, which isn't on the allowlist, so it's rejected — the same way any other unrecognized node is. The security property comes from the shape of what's permitted, not from enumerating what's forbidden.&lt;/p&gt;

&lt;p&gt;Server-side JS operators (&lt;code&gt;$where&lt;/code&gt;, &lt;code&gt;$function&lt;/code&gt;, &lt;code&gt;$accumulator&lt;/code&gt;) and cross-collection writes (&lt;code&gt;$out&lt;/code&gt;, &lt;code&gt;$merge&lt;/code&gt;) are separately blocked, because those move execution back to mongod where our parser has no say.&lt;/p&gt;

&lt;h2&gt;
  
  
  What generalizes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Sandboxed eval" is usually a contradiction.&lt;/strong&gt; If your threat model includes the user being hostile, the question isn't which sandbox — it's whether you can avoid evaluating their code at all. For anything expression-shaped, you usually can.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never allowlist over source text.&lt;/strong&gt; Allowlist over parsed structure. Text has infinite spellings; an AST has a finite node vocabulary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deny by default at the node level.&lt;/strong&gt; "Unknown construct → reject" is a one-line invariant. "Known-bad construct → reject" is a list you will be maintaining against attackers forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Count what's reachable from inside, not just what's exposed.&lt;/strong&gt; The regex was guarding the method names. The actual exposure was the live &lt;code&gt;db&lt;/code&gt; handle sitting in scope next to them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have a "users write a small expression" feature running through &lt;code&gt;vm&lt;/code&gt;, &lt;code&gt;eval&lt;/code&gt;, or &lt;code&gt;new Function&lt;/code&gt; — that's the one. Go look at it.&lt;/p&gt;

</description>
      <category>security</category>
      <category>javascript</category>
      <category>node</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Enforcing subscription plan limits in NestJS without scattering `if (plan === 'PRO')` everywhere</title>
      <dc:creator>cursora</dc:creator>
      <pubDate>Sat, 25 Jul 2026 07:56:25 +0000</pubDate>
      <link>https://dev.to/cursora/enforcing-subscription-plan-limits-in-nestjs-without-scattering-if-plan-pro-everywhere-2flb</link>
      <guid>https://dev.to/cursora/enforcing-subscription-plan-limits-in-nestjs-without-scattering-if-plan-pro-everywhere-2flb</guid>
      <description>&lt;h2&gt;
  
  
  The problem with the obvious approach
&lt;/h2&gt;

&lt;p&gt;The first version of plan enforcement in any product looks the same: a controller method starts with a check.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BASIC&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;courseCount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ForbiddenException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Upgrade to create more courses&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works, for exactly as long as there is one such rule. Ours has several — a cap on total courses, a separate cap on how many of those may be &lt;em&gt;paid&lt;/em&gt;, a cap on standalone lessons — and each applies at a different endpoint. Copy that check into six controllers and you have six places to update when a limit changes, and one of them will be missed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we did instead: a decorator that names the action
&lt;/h2&gt;

&lt;p&gt;Enforcement moved into a guard, and controllers only declare &lt;em&gt;which&lt;/em&gt; limited action they perform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;EnforcePlanLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CREATE_COURSE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;createCourse&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CreateCourseDto&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The decorator is just metadata plus the guard, composed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;EnforcePlanLimit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PlanLimitAction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nf"&gt;applyDecorators&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;SetMetadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PLAN_LIMIT_ACTION_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;UseGuards&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PlanLimitGuard&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The guard reads that metadata with &lt;code&gt;Reflector&lt;/code&gt;, resolves the user, and delegates to a single service that owns every limit rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reflector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getAllAndOverride&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PlanLimitAction&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BYPASS&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;PLAN_LIMIT_ACTION_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getHandler&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getClass&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BYPASS&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;planLimitsService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enforceOrThrow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now a limit change is a one-file change. The controllers never learn the numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that isn't in the tutorials: conditional enforcement
&lt;/h2&gt;

&lt;p&gt;The interesting cases are the ones where the &lt;em&gt;same endpoint&lt;/em&gt; is sometimes limited and sometimes not.&lt;/p&gt;

&lt;p&gt;Marking a course as paid is limited — but the endpoint that does it is a general update endpoint that also handles a dozen unlimited edits. Blocking it wholesale would mean free-tier users can't rename their own courses. So the guard inspects the request before deciding to enforce at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;shouldEnforce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PlanLimitAction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SET_COURSE_PAID&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isPaid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;isPaid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;isPaid&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;isPaid&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same story for standalone lessons: a lesson created &lt;em&gt;inside&lt;/em&gt; a course doesn't count against the standalone-lesson cap, and the only way to tell them apart is whether &lt;code&gt;courseId&lt;/code&gt; is present in the body. That check lives in the guard too, next to the rule it belongs to, rather than being smeared across the lesson service.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;=== 'true'&lt;/code&gt; string comparison is not an accident either — multipart form bodies deliver booleans as strings, and a limit that silently stops applying for one content type is worse than no limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  An explicit escape hatch beats an implicit one
&lt;/h2&gt;

&lt;p&gt;Some routes must never be limited — admin tooling, internal migrations. Rather than let those quietly work because nobody remembered to add the decorator, there's an explicit opt-out that reads as a deliberate decision in review:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BypassPlanLimit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;SetMetadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PLAN_LIMIT_ACTION_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BYPASS&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"No decorator" and "deliberately unlimited" look identical in a diff otherwise. They shouldn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this bought us
&lt;/h2&gt;

&lt;p&gt;When we published our pricing publicly this week — free tier: 3 courses, free-only; paid tiers lifting the course, paid-course and lesson caps — the numbers in the marketing copy came from one service, and changing them meant editing one file. That's the actual payoff: the pricing page and the enforcement can't drift apart, because there's only one place that knows.&lt;/p&gt;

&lt;p&gt;Curious how others handle the conditional case — enforcement that depends on the request body, not just the route. Guard, interceptor, or push it down into the service? Interested in arguments for the other two.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>typescript</category>
      <category>architecture</category>
      <category>backend</category>
    </item>
    <item>
      <title>Grading a scanned, handwritten answer sheet with OCR: harder than it sounds</title>
      <dc:creator>cursora</dc:creator>
      <pubDate>Thu, 23 Jul 2026 18:55:45 +0000</pubDate>
      <link>https://dev.to/cursora/grading-a-scanned-handwritten-answer-sheet-with-ocr-harder-than-it-sounds-2cn1</link>
      <guid>https://dev.to/cursora/grading-a-scanned-handwritten-answer-sheet-with-ocr-harder-than-it-sounds-2cn1</guid>
      <description>&lt;h2&gt;
  
  
  The feature that sounded simple in the spec
&lt;/h2&gt;

&lt;p&gt;"Let teachers scan a paper test and have it auto-graded." That was the one-line spec for a feature we shipp&lt;br&gt;
ed in Cursora Assess, our grading tool. It sounded like a weekend project. It was not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why printed multiple-choice OCR is the easy 80%
&lt;/h2&gt;

&lt;p&gt;If every test were a single canonical layout with clean printed bubbles, this is a solved problem -- that's&lt;br&gt;
 exactly what commercial scantron hardware has done since the 1970s. Our first version handled that fine: g&lt;br&gt;
enerate a test as a PDF with a fixed answer-grid layout, print it, scan it back, and a well-tuned OCR/bubbl&lt;br&gt;
e-detection pass gets you close to 100% accuracy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it actually gets hard
&lt;/h2&gt;

&lt;p&gt;Two things blow up the easy version:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Randomized variants.&lt;/strong&gt; To make cheating-by-glancing-at-your-neighbor harder, Assess generates multiple sh&lt;br&gt;
uffled variants of the same test -- different question order, different answer order per question. That mea&lt;br&gt;
ns the grading pass can't assume a fixed answer key position; it has to read a variant identifier off the s&lt;br&gt;
heet first, then grade against the &lt;em&gt;matching&lt;/em&gt; key, not just "the" key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real-world scans, not clean lab conditions.&lt;/strong&gt; Teachers photograph these with a phone, at an angle, someti&lt;br&gt;
mes in bad lighting, sometimes slightly creased. A bubble that's 90% filled versus 40% filled versus "start&lt;br&gt;
ed to fill then scribbled out" all need different handling, and a naive brightness threshold falls over imm&lt;br&gt;
ediately once the photo isn't perfectly lit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually worked
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Perspective correction before anything else -- detect the page's corner markers and de-skew/de-warp the i
mage so the grid math downstream can assume a roughly rectangular, aligned sheet&lt;/li&gt;
&lt;li&gt;Per-bubble fill-ratio scoring rather than a single global threshold, so lighting gradients across one pho
to don't tank accuracy on one side of the page&lt;/li&gt;
&lt;li&gt;Variant ID read as its own first-class OCR step, gating which answer key the rest of the pipeline grades
against&lt;/li&gt;
&lt;li&gt;A confidence score per answer, not just a binary read -- low-confidence bubbles get flagged for the teach
er to eyeball rather than silently guessed&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The honest caveat
&lt;/h2&gt;

&lt;p&gt;This isn't magic, and we don't pretend it is: genuinely ambiguous marks (half-erased, two bubbles filled) s&lt;br&gt;
till get flagged for a human to resolve rather than auto-resolved with a coin flip. Grading in seconds inst&lt;br&gt;
ead of an evening doesn't require 100% unattended accuracy -- it requires being right when it's confident a&lt;br&gt;
nd honest when it's not.&lt;/p&gt;

&lt;p&gt;If you've built anything in this space (form OCR, scantron-alternative pipelines, handwriting recognition)&lt;br&gt;
I'd like to compare notes -- drop it in the comments.&lt;/p&gt;

</description>
      <category>ocr</category>
      <category>computervision</category>
      <category>software</category>
      <category>showdev</category>
    </item>
    <item>
      <title>A real Docker exercise from our course platform (try it yourself)</title>
      <dc:creator>cursora</dc:creator>
      <pubDate>Wed, 22 Jul 2026 19:06:23 +0000</pubDate>
      <link>https://dev.to/cursora/a-real-docker-exercise-from-our-course-platform-try-it-yourself-477a</link>
      <guid>https://dev.to/cursora/a-real-docker-exercise-from-our-course-platform-try-it-yourself-477a</guid>
      <description>&lt;h2&gt;
  
  
  Skip the pitch, here's the exercise
&lt;/h2&gt;

&lt;p&gt;Rather than describe our coding education platform in the abst&lt;br&gt;
ract, I want to show one actual exercise, word for word (trans&lt;br&gt;
lated -- the course itself is Polish-first), from the "Cloud T&lt;br&gt;
echnologies" course. Try it yourself if you want; it only need&lt;br&gt;
s Docker.&lt;/p&gt;
&lt;h2&gt;
  
  
  The scenario
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;You are a junior DevOps engineer at a tech company. Your tea&lt;br&gt;
m lead has asked you to start a simple HTTP server in a Docker&lt;br&gt;
 container for a test environment. The server must run with a&lt;br&gt;
specific configuration -- verify it using Docker's diagnostic&lt;br&gt;
tools.&lt;/p&gt;

&lt;p&gt;Start a container named &lt;code&gt;web_server&lt;/code&gt; using the &lt;code&gt;python:3.11-&lt;br&gt;
slim&lt;/code&gt; image, in detached mode. The container should run Python&lt;br&gt;
's built-in HTTP server on port 8000 (command: &lt;code&gt;python3 -m htt&lt;br&gt;
p.server 8000&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Configuration requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Container port 8000 must be accessible on host port 8001&lt;/li&gt;
&lt;li&gt;Environment variable &lt;code&gt;APP_ENV&lt;/code&gt; set to &lt;code&gt;production&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you want to try it cold before reading on: that's the whole&lt;br&gt;
 prompt. Here's a solution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; web_server &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 8001:8000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;APP_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;production &lt;span class="se"&gt;\&lt;/span&gt;
  python:3.11-slim &lt;span class="se"&gt;\&lt;/span&gt;
  python3 &lt;span class="nt"&gt;-m&lt;/span&gt; http.server 8000

docker ps
docker inspect web_server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exercise doesn't stop at "make it run" -- the follow-up as&lt;br&gt;
ks you to explain what each field in &lt;code&gt;docker inspect&lt;/code&gt;'s output&lt;br&gt;
 actually means (&lt;code&gt;State&lt;/code&gt;, &lt;code&gt;NetworkSettings.Ports&lt;/code&gt;, &lt;code&gt;Config.Env&lt;br&gt;
&lt;/code&gt;, etc.), which is the part that actually separates "I copied&lt;br&gt;
a command" from "I understand what's happening."&lt;/p&gt;

&lt;h2&gt;
  
  
  What's running underneath it
&lt;/h2&gt;

&lt;p&gt;This isn't a fake terminal with pre-recorded output. The actua&lt;br&gt;
l sandbox config for this exercise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Base image: &lt;code&gt;alpine:3.20&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;CPU limit: 0.5 cores&lt;/li&gt;
&lt;li&gt;Execution mode: isolated VM worker&lt;/li&gt;
&lt;li&gt;Network access: &lt;code&gt;none&lt;/code&gt; by default (no outbound calls unless
an exercise specifically opens it)&lt;/li&gt;
&lt;li&gt;Optional bash setup script runs before the student's first c
ommand&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I'm posting this instead of a feature list
&lt;/h2&gt;

&lt;p&gt;Screenshots of a dashboard don't tell you anything about wheth&lt;br&gt;
er the actual course content is any good. One real exercise, r&lt;br&gt;
eproduced exactly, does. This is one lesson out of many in one&lt;br&gt;
 course out of 27 currently in the catalog at &lt;a href="ht&lt;br&gt;%0Atps://cursora.org"&gt;cursora.org&lt;/a&gt;. Happy to answer questions about the sandbo&lt;br&gt;
x architecture or the exercise design specifically.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>tutorial</category>
      <category>beginners</category>
    </item>
    <item>
      <title>I stopped watching coding tutorials. Here's what I do instead.</title>
      <dc:creator>cursora</dc:creator>
      <pubDate>Tue, 21 Jul 2026 19:21:19 +0000</pubDate>
      <link>https://dev.to/cursora/i-stopped-watching-coding-tutorials-heres-what-i-do-instead-3hdn</link>
      <guid>https://dev.to/cursora/i-stopped-watching-coding-tutorials-heres-what-i-do-instead-3hdn</guid>
      <description>&lt;h2&gt;
  
  
  The tutorial graveyard
&lt;/h2&gt;

&lt;p&gt;Be honest with yourself for a second: how many "Complete Python Course" or "Learn Docker in 2 Hours" videos do you have sitting half-watched in a browser tab or a bookmarks folder right now? For me it used to be a lot. I'd watch someone type, nod along, feel like I understood it, and then sit down to actually build something and realize I couldn't reproduce a single line without the video paused next to me.&lt;/p&gt;

&lt;p&gt;That's not a discipline problem. Watching someone perform a skill and performing it yourself are genuinely different cognitive tasks. Tutorials are great at the first one and mostly useless at the second.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed for me
&lt;/h2&gt;

&lt;p&gt;I started deliberately picking learning resources where I couldn't just watch -- where the platform forced me to actually write and run code as part of the lesson, not as an optional "now try it yourself" afterthought tacked onto the end of a video. cursora.org (the platform I've been using, full disclosure I also work on the marketing side of it) structures every hands-on course this way: the lesson &lt;em&gt;is&lt;/em&gt; a real, isolated sandbox -- a real container, real CPU/memory limits, real command output -- not a video with a code snippet next to it.&lt;/p&gt;

&lt;p&gt;The difference sounds small until you notice how much it changes your behavior. You can't half-pay-attention to an exercise you have to actually solve. You can't "I'll rewatch that part later" your way past a Docker command that just failed in front of you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually looks like
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;27 courses right now, spanning web dev, backend, DevOps and more&lt;/li&gt;
&lt;li&gt;Every coding exercise runs in an isolated sandbox, not a simulated terminal&lt;/li&gt;
&lt;li&gt;The catalog is browsable for free, no signup, so you can see exactly what a lesson looks like before committing&lt;/li&gt;
&lt;li&gt;A completion certificate at the end, if that matters for your situation&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The actual point of this post
&lt;/h2&gt;

&lt;p&gt;I'm not going to pretend one platform is a silver bullet -- plenty of people learn fine from videos, and plenty of hands-on platforms exist. But if you recognize yourself in the first paragraph of this post, the fix probably isn't "watch better tutorials." It's picking something that doesn't let you get away with just watching. Worth a look: cursora.org&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>career</category>
      <category>beginners</category>
      <category>productivity</category>
    </item>
    <item>
      <title>What a coding education platform actually looks like after 6 months of building (screenshots, no mockups)</title>
      <dc:creator>cursora</dc:creator>
      <pubDate>Mon, 20 Jul 2026 19:29:34 +0000</pubDate>
      <link>https://dev.to/cursora/what-a-coding-education-platform-actually-looks-like-after-6-months-of-building-screenshots-no-239b</link>
      <guid>https://dev.to/cursora/what-a-coding-education-platform-actually-looks-like-after-6-months-of-building-screenshots-no-239b</guid>
      <description>&lt;h2&gt;
  
  
  Building in public, screenshots included
&lt;/h2&gt;

&lt;p&gt;I work on &lt;a href="https://cursora.org" rel="noopener noreferrer"&gt;cursora&lt;/a&gt;, a coding education platform. Landing pages tend to oversell — so instead of another feature list, here's what the product actually looks like right now, straight from the running app.&lt;/p&gt;

&lt;h2&gt;
  
  
  The catalog
&lt;/h2&gt;

&lt;p&gt;27 published courses live today, spanning web, backend, DevOps and more. No "coming soon" placeholders in the grid — these are courses students can start immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  The instructor editor
&lt;/h2&gt;

&lt;p&gt;Courses are built section by section, lesson by lesson, with a live "readiness" score so instructors can see how close a course is to publish-ready before it goes live. Our own DevOps course is sitting at 90% readiness as I write this.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sandbox
&lt;/h2&gt;

&lt;p&gt;This is the part I actually care about explaining. Coding exercises don't run in a fake terminal — each one spins up an isolated container with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A real base image (&lt;code&gt;alpine:3.20&lt;/code&gt; for this exercise)&lt;/li&gt;
&lt;li&gt;Explicit CPU limits (0.5 cores here) and a memory ceiling per container&lt;/li&gt;
&lt;li&gt;A configurable execution mode — VM-isolated workers for the heavier exercises&lt;/li&gt;
&lt;li&gt;Network access set to &lt;code&gt;none&lt;/code&gt; by default — no external calls in or out&lt;/li&gt;
&lt;li&gt;An optional setup script (plain bash) that provisions the environment before the student's first command&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Students run real commands — &lt;code&gt;docker version&lt;/code&gt;, &lt;code&gt;docker info&lt;/code&gt;, whatever the exercise calls for — against a real environment, not a canned output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cursora Assess
&lt;/h2&gt;

&lt;p&gt;For instructors who still want traditional graded tests alongside the hands-on exercises, there's Cursora Assess: generate printable/online test variants, auto-grade scanned answer sheets via OCR, and get a stats/report view per group.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it's at
&lt;/h2&gt;

&lt;p&gt;669 learners, 27 courses, and a platform that's still very much being built in the open. If you're curious what practical, hands-on coding education infrastructure looks like under the hood, take a look: cursora.org. Happy to answer questions about the sandbox architecture specifically — that's the part I'd most like feedback on. &lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi5irk75tgspmwcjvafoj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi5irk75tgspmwcjvafoj.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>showdev</category>
      <category>docker</category>
    </item>
    <item>
      <title>I Built a Platform Where Anyone Can Publish a Coding Course Today — No Approval Process</title>
      <dc:creator>cursora</dc:creator>
      <pubDate>Mon, 20 Jul 2026 05:36:36 +0000</pubDate>
      <link>https://dev.to/cursora/i-built-a-platform-where-anyone-can-publish-a-coding-course-today-no-approval-process-1m7d</link>
      <guid>https://dev.to/cursora/i-built-a-platform-where-anyone-can-publish-a-coding-course-today-no-approval-process-1m7d</guid>
      <description>&lt;h2&gt;
  
  
  Why I'm writing this
&lt;/h2&gt;

&lt;p&gt;I work on &lt;a href="https://cursora.org" rel="noopener noreferrer"&gt;cursora&lt;/a&gt;, a coding education platform. We're trying to grow the course catalog, and instead of a polished landing page pitch, I'd rather explain honestly what it actually is — and ask this community to poke holes in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The premise
&lt;/h2&gt;

&lt;p&gt;Most "become an instructor" flows on ed-tech platforms involve an application, a review queue, and a wait. Ours doesn't. If you're logged in, you can start building a course right now at &lt;code&gt;cursora.org/my/teaching&lt;/code&gt; — no approval gate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you actually get
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A content editor with full control over your material — text, images, video, code&lt;/li&gt;
&lt;li&gt;Real code execution sandboxes for 13 languages (Python, JS/TS, Go, Rust, Java, C/C++, and more)&lt;/li&gt;
&lt;li&gt;Challenge types beyond multiple-choice: Linux/CLI challenges, MongoDB challenges, CSS challenges, and full project-based tasks&lt;/li&gt;
&lt;li&gt;Cursora Assess, a proper tool for testing and grading students — not just a quiz widget&lt;/li&gt;
&lt;li&gt;Full control over your course's price and content&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's honestly still missing
&lt;/h2&gt;

&lt;p&gt;I'm not going to oversell this: we don't have a slick, fully worked-out payout/commission system yet. That part of the platform is still evolving. If your main motivation is monetizing a course today, it's fair to wait. If you like teaching and want a place to publish something more interactive than a slide deck or a YouTube playlist, it's worth a look.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why hands-on matters to us
&lt;/h2&gt;

&lt;p&gt;The platform's whole premise is that people learn by doing, not by watching. So the course-authoring tools are built around that: real sandboxes instead of video-only lessons, real challenge types instead of just quizzes.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want to try it
&lt;/h2&gt;

&lt;p&gt;Start here: &lt;code&gt;cursora.org/my/teaching&lt;/code&gt;. No form, no waiting on someone's approval queue. Happy to answer hard questions in the comments — including "why would I use this over [X]."&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>firstyearincode</category>
      <category>programmers</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Your Team Has an Internal Onboarding Doc for Juniors? That's Already 80% of a Course</title>
      <dc:creator>cursora</dc:creator>
      <pubDate>Sat, 18 Jul 2026 06:25:10 +0000</pubDate>
      <link>https://dev.to/cursora/your-team-has-an-internal-onboarding-doc-for-juniors-thats-already-80-of-a-course-h3c</link>
      <guid>https://dev.to/cursora/your-team-has-an-internal-onboarding-doc-for-juniors-thats-already-80-of-a-course-h3c</guid>
      <description>&lt;p&gt;If you've ever onboarded a junior on your team, you probably have a doc — or three — sitting in Confluence, Notion, or some private repo, explaining how your team does code review, how to set up the dev environment, what mistakes to avoid, what your commit convention looks like. That knowledge lives in an internal wiki and never leaves it.&lt;/p&gt;

&lt;p&gt;That's exactly the kind of material that turns into a course.&lt;/p&gt;

&lt;p&gt;The gap between "internal doc" and "course" is smaller than it looks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A text doc → a lesson with tables, code blocks, and Mermaid diagrams if you're explaining architecture or a data flow.&lt;/li&gt;
&lt;li&gt;"Try setting this up and let me know how it goes" → a code challenge that actually checks whether the student can do it, run in a sandbox in one of 13 supported languages — no more "done ✅" on Slack, the result gets checked automatically.&lt;/li&gt;
&lt;li&gt;Your three most-repeated pieces of code review feedback → a standalone lesson on the mistakes you always see from new hires.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don't need to start from zero and invent a curriculum. The material already exists — it just needs to come out of the internal wiki, get organized, and turn into something someone outside the company can work through too.&lt;/p&gt;

&lt;p&gt;Bonus: next time you hire a junior, you can send a link instead of explaining the same thing live all over again.&lt;/p&gt;

&lt;p&gt;Set up an instructor account on &lt;a href="https://cursora.org" rel="noopener noreferrer"&gt;cursora.org&lt;/a&gt; — see how much of what you already know and already have written down turns into a first lesson in under an hour.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
