<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/rss.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Serhii Soldatov</title><description>Notes and essays on building software.</description><link>https://serhiisoldatov.dev/</link><item><title>Decomposing hardcoded roles: rewriting access control in a Django app without breaking customers</title><link>https://serhiisoldatov.dev/blog/decomposing-hardcoded-roles/</link><guid isPermaLink="true">https://serhiisoldatov.dev/blog/decomposing-hardcoded-roles/</guid><description>How we replaced three hardcoded roles with 46 named permissions and customer-defined roles, and proved nothing changed before changing anything.</description><pubDate>Sat, 31 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Our Django monolith runs a no-code workflow product, and every user in it was one of three hardcoded roles: &lt;code&gt;admin&lt;/code&gt;, &lt;code&gt;member&lt;/code&gt; or &lt;code&gt;light_user&lt;/code&gt;. That worked until customers started asking for something in between, like a person who manages integrations without being a full admin. Each of those requests meant a code change on our side, and usually the answer was “make them an admin”, which handed over every other admin power too.&lt;/p&gt;
&lt;p&gt;Over about three and a half months we replaced those roles with a catalog of 46 named permissions, let customers build their own roles from it, and shipped it to production. Existing roles kept doing what they did before, apart from a few changes we made on purpose.&lt;/p&gt;
&lt;p&gt;Here is how we did it, and the rule that kept production safe throughout: prove nothing changed, then change things.&lt;/p&gt;
&lt;h2 id=&quot;the-legacy-problem-one-question-four-places-to-answer-it&quot;&gt;The legacy problem: one question, four places to answer it&lt;a href=&quot;#the-legacy-problem-one-question-four-places-to-answer-it&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: The legacy problem: one question, four places to answer it&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Each organization membership stored one of three role strings, and access decisions were role predicates like &lt;code&gt;is_organization_admin&lt;/code&gt; or &lt;code&gt;is_light_user&lt;/code&gt;. Each of them asked “which role are you?”, never “what are you allowed to do?”.&lt;/p&gt;
&lt;p&gt;Worse, those checks lived in four different layers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;django-rules registries&lt;/li&gt;
&lt;li&gt;DRF permission classes&lt;/li&gt;
&lt;li&gt;queryset filters deciding which rows a user sees, which duplicated the rules logic as raw &lt;code&gt;Q&lt;/code&gt; objects&lt;/li&gt;
&lt;li&gt;ad-hoc inline role checks in views, serializers and services&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On top of the all-or-nothing admin, this created three more problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Hardcoded exceptions:&lt;/strong&gt; The light user was not a set of powers but about 15 hardcoded branches and bespoke screens spread across several apps. Changing it was fragile, and generalizing it was impossible.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Implicit checks:&lt;/strong&gt; Some member powers had no check at all, because the code assumed “anyone who isn’t a light user can”. You can’t refactor a check that doesn’t exist.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Documentation drift:&lt;/strong&gt; Nobody had a single table of what each role could actually do, and the docs and the code already disagreed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Replacing the role checks and introducing custom roles in one step would have made regressions difficult to detect. Before changing the authorization model, we needed a way to prove what the application allowed (and what it denied) today.&lt;/p&gt;
&lt;h2 id=&quot;the-arc-how-we-got-there&quot;&gt;The arc: how we got there&lt;a href=&quot;#the-arc-how-we-got-there&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: The arc: how we got there&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;We split the project into five phases:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Safety-net test grid:&lt;/strong&gt; establish coverage before changing the authorization code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Admin decomposition:&lt;/strong&gt; replace hardcoded admin checks with explicit permission codes, while seeding the legacy roles to preserve their existing behavior.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Member decomposition:&lt;/strong&gt; do the same for everyday member permissions before customers could build roles on top of the catalog.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Custom roles:&lt;/strong&gt; let customers define roles from the catalog and assign them, behind a per-organization feature flag.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Light user decomposition:&lt;/strong&gt; express restricted-user access through the new model, so customers can build roles like “a light user who can also see workflows”.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;prove-nothing-changed-before-changing-anything&quot;&gt;Prove nothing changed before changing anything&lt;a href=&quot;#prove-nothing-changed-before-changing-anything&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Prove nothing changed before changing anything&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;We wanted to replace the authorization mechanism without changing what customers could do. Instead of hiding the refactor behind a feature flag, we built an allow/deny test grid for every registered permission, and the grid staying green was the proof.&lt;/p&gt;
&lt;p&gt;The deny side is what matters. A test that only checks “the admin can do X” still passes after someone deletes the restriction. So a typical entry checks one permission from three sides:&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;python&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; test_change_process&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt;admin&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt; member&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt; another_member&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt; process&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;):&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;    # process.lead = member&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    assert&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; admin&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;has_perm&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;processor.change_process&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;, process)&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;              # allow: role&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    assert&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; member&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;has_perm&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;processor.change_process&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;, process)&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;             # allow: workflow lead&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    assert&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; not&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; another_member&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;has_perm&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;processor.change_process&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;, process)&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;  # deny&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;We wrote these against the old role checks, and they had to pass unchanged after every refactoring step.&lt;/p&gt;
&lt;p&gt;The guard compares the grid with the live django-rules registry, so a new permission without tests fails CI:&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;python&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; test_registry_matches_grid&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;():&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    assert&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; set&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(rules_registry.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;keys&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;())&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; ==&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; REGISTERED_PERMISSIONS&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;The feature flag came back only for custom roles, the first change customers could actually see.&lt;/p&gt;
&lt;h2 id=&quot;separating-roles-from-relationships&quot;&gt;Separating roles from relationships&lt;a href=&quot;#separating-roles-from-relationships&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Separating roles from relationships&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Not every access question is about roles. “Can this role delete workflows?” and “Is this user the lead of this workflow?” are different questions, so we kept them apart:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Role permissions&lt;/strong&gt; decide what a user can generally do.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Relationships&lt;/strong&gt; decide how a user relates to a specific item: workflow lead, step assignee, team member, folder collaborator.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The new system answers only the role half, through one helper:&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;python&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;user&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;has_org_permission&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(org_id, &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;workflow.delete&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;The same lookup backs a django-rules predicate and a DRF permission class, so every layer asks the same question. At the call site, the two halves are OR-ed:&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;python&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;can_delete &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; has_perm&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;workflow.delete&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; |&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; is_workflow_lead&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;Relationship checks stayed exactly as they were. That kept the refactor small, and it’s why a role without “View all workflows” still sees the workflows its user is involved in.&lt;/p&gt;
&lt;p&gt;One more decision removed a whole class of bugs: the admin role holds every member permission plus the admin-only ones, so no check needs an extra “or is admin” branch.&lt;/p&gt;
&lt;h2 id=&quot;defining-the-catalog-from-code-to-frontend&quot;&gt;Defining the catalog: from code to frontend&lt;a href=&quot;#defining-the-catalog-from-code-to-frontend&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Defining the catalog: from code to frontend&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Every permission is declared in code as a catalog entry. An idempotent seed command syncs the catalog to the database on every deploy.&lt;/p&gt;
&lt;p&gt;A single permission looks like this:&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;python&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;CatalogPermission&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;    code&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;team.view&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;    name&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;View all teams&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;    resource&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;Teams&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;    read_write&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;ReadWrite.READ,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;    description&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;See every team in the organization, not only your own&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;    group&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;_G.WORKSPACE,  &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# section in the role editor&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;    classification&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;_C.MEMBER_AND_ADMIN,  &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# built-in roles that get it by default&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;    requires&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;workspace.overview&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;team.overview&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;),  &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# auto-ticked together&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;The catalog has three kinds of codes, and this example touches two of them:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Page-display codes (&lt;code&gt;*.overview&lt;/code&gt;)&lt;/strong&gt; show or hide a section of the app, like the Teams page.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;“View all” codes (&lt;code&gt;*.view&lt;/code&gt;)&lt;/strong&gt; let a user see every item in the organization, not only the ones they are related to.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Action codes&lt;/strong&gt; (&lt;code&gt;create&lt;/code&gt;, &lt;code&gt;manage&lt;/code&gt;, &lt;code&gt;delete&lt;/code&gt;, &lt;code&gt;export&lt;/code&gt;, …) let a user change things.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;requires&lt;/code&gt; list defines dependencies between permissions. Ticking “View all teams” in the role editor also ticks the Workspace and Teams pages it depends on, and the server rejects any role that holds a code without its requirements.&lt;/p&gt;
&lt;h2 id=&quot;the-data-model&quot;&gt;The data model&lt;a href=&quot;#the-data-model&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: The data model&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Three tables carry the whole system:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Permission&lt;/code&gt;: one row per catalog code, with its name, description, group and &lt;code&gt;requires&lt;/code&gt; list.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Role&lt;/code&gt;: a named set of permissions. System roles (&lt;code&gt;admin&lt;/code&gt;, &lt;code&gt;member&lt;/code&gt;, &lt;code&gt;light_user&lt;/code&gt;) have no organization and are shared by everyone, while custom roles belong to one customer organization. Each role has a stable &lt;code&gt;code&lt;/code&gt; that identity providers can map onto.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RolePermission&lt;/code&gt;: the join table mapping permissions to roles.&lt;/li&gt;
&lt;/ul&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;Role ↔ Permission&lt;/span&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot; role=&quot;img&quot; aria-label=&quot;Diagram: Role and Permission are many-to-many, joined by the RolePermission table (role_id, permission_id).&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;       +-------------------+             +-----------------------+&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;       |       Role        |             |      Permission       |&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;       +-------------------+             +-----------------------+&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;       | id                |             | id                    |&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;       | code              |             | code                  |&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;       | name              |             | group                 |&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;       | organization_id   |             | requires              |&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;       | is_system         |             +-----------+-----------+&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;       +---------+---------+                         |&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;                 |                                   |&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;                 |       +-------------------+       |&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;                 +------&amp;gt;|  RolePermission   |&amp;lt;------+&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;                         +-------------------+&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;                         | role_id           |&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;                         | permission_id     |&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;                         +-------------------+&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;Memberships still store the old role string, which resolves to the matching system role. A custom role replaces it only on a &lt;code&gt;member&lt;/code&gt; membership in an organization with custom roles enabled, so organizations without the flag see no change at all. Every change to roles, their permissions and role assignments is recorded with django-simple-history.&lt;/p&gt;
&lt;h3 id=&quot;granularity-strategy-start-coarse&quot;&gt;Granularity strategy: start coarse&lt;a href=&quot;#granularity-strategy-start-coarse&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Granularity strategy: start coarse&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;We started with coarse permissions and split them only when the existing authorization logic required a distinction.&lt;/p&gt;
&lt;p&gt;Splitting a permission later is relatively straightforward. Merging permissions is harder: once customers have custom roles, someone may have one permission enabled and the other disabled. Combining them means deciding what happens to those saved choices.&lt;/p&gt;
&lt;p&gt;That’s why API keys, folders, members and teams ended up with a single &lt;code&gt;*.manage&lt;/code&gt; permission rather than separate edit and delete permissions. Running workflows needed finer distinctions, so we split those actions.&lt;/p&gt;
&lt;h3 id=&quot;new-permissions-vs-saved-custom-roles&quot;&gt;New permissions vs. saved custom roles&lt;a href=&quot;#new-permissions-vs-saved-custom-roles&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: New permissions vs. saved custom roles&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Once customers save roles, every new permission raises a question: should existing custom roles get it? We can’t know whether a customer meant a role to be powerful or locked down, so the seed never touches custom roles.&lt;/p&gt;
&lt;p&gt;Instead, a new permission reaches them through an explicit, reversible data migration that pins it to an existing one. When we split stopping a running workflow out of editing it, every custom role that could already edit (and so could already stop) received the new stop permission. Nobody gained or lost a power.&lt;/p&gt;
&lt;h3 id=&quot;performance-controls&quot;&gt;Performance controls&lt;a href=&quot;#performance-controls&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Performance controls&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Permission checks can happen many times during a single request, so we resolve the user’s effective permission set once per request and memoize it on the user object.&lt;/p&gt;
&lt;p&gt;We also deliberately left the permission tables out of ORM-level caching, so a role edit takes effect on the next request without a cache-invalidation mechanism.&lt;/p&gt;
&lt;h2 id=&quot;decomposing-the-light-user&quot;&gt;Decomposing the light user&lt;a href=&quot;#decomposing-the-light-user&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Decomposing the light user&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Admin and member mostly differed in &lt;em&gt;which&lt;/em&gt; checks they passed. The light user was different: a separate code path with its own branches and screens, making it the hardest of the three roles to fold into the permission system.&lt;/p&gt;
&lt;p&gt;The obvious options all had the same flaw. Letting a custom role start from a “base role” (member or light user), or adding a &lt;code&gt;base_role&lt;/code&gt; column, would keep role-string checks and permission checks alive side by side forever. One permission per hardcoded site would have meant about ten codes for what was mostly the same idea.&lt;/p&gt;
&lt;p&gt;The key was identifying the light user’s defining trait: they only see content they’re directly involved in, not organization-wide content. We expressed that through a single permission, &lt;code&gt;organization.view_public_content&lt;/code&gt;. Admin and member have it; light user doesn’t. Without it, visibility stays limited by relationships, such as assigned workflow steps.&lt;/p&gt;
&lt;p&gt;Customers can now create a “near light user”: a custom role that leaves this permission disabled while adding whatever other capabilities they need. The real light user role behaves exactly as before, and the change needed no schema migration.&lt;/p&gt;
&lt;h2 id=&quot;frontend-controls-and-user-experience&quot;&gt;Frontend controls and user experience&lt;a href=&quot;#frontend-controls-and-user-experience&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Frontend controls and user experience&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The frontend needs to reflect the backend’s authorization decisions without becoming a second source of truth.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The UI follows permissions.&lt;/strong&gt; The backend returns the user’s permissions for the active organization. The frontend uses them to show or hide navigation items and actions, rather than checking the user’s role name.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Hiding isn’t security.&lt;/strong&gt; Without an &lt;code&gt;*.overview&lt;/code&gt; permission, a section disappears from the navigation, but the API and links from related items keep working. The backend must still check permissions and filter data.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Unavailable actions:&lt;/strong&gt; if a user can’t perform an action, the UI disables the corresponding control and explains why.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Users can’t change their own role, not even admins.&lt;/strong&gt; Otherwise a custom role allowed to manage members could promote itself to admin.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;lessons-learned&quot;&gt;Lessons learned&lt;a href=&quot;#lessons-learned&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Lessons learned&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The biggest lesson was to separate the behavior-preserving refactor from the product change. We replaced the authorization mechanism first, proven by a deny-heavy test grid, then introduced custom roles behind a feature flag.&lt;/p&gt;
&lt;p&gt;The second lesson: the catalog becomes a contract the day a customer saves a role. Start coarse, and decide how new permissions reach existing custom roles before you need to.&lt;/p&gt;
</content:encoded><category>django</category><category>python</category><category>architecture</category><category>access-control</category></item><item><title>Migrating a production Django app from Elasticsearch to OpenSearch</title><link>https://serhiisoldatov.dev/blog/elasticsearch-to-opensearch-migration-story/</link><guid isPermaLink="true">https://serhiisoldatov.dev/blog/elasticsearch-to-opensearch-migration-story/</guid><description>How a one-line urllib3 security bump turned into migrating a Django app from elasticsearch-py to opensearch-py, and what we vendored to keep the API responses unchanged.</description><pubDate>Fri, 25 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;key-takeaways&quot;&gt;Key takeaways&lt;a href=&quot;#key-takeaways&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Key takeaways&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Legacy search clients like &lt;code&gt;elasticsearch==7.10.1&lt;/code&gt; hard-cap underlying transport libraries (&lt;code&gt;urllib3&amp;lt;2&lt;/code&gt;), blocking critical security patches for vulnerabilities like &lt;strong&gt;CVE-2025-50181&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;elasticsearch-py 7.14.0+&lt;/code&gt; introduced a client-side &lt;code&gt;UnsupportedProductError&lt;/code&gt; check that refuses connections to non-Elasticsearch servers, making &lt;code&gt;opensearch-py&lt;/code&gt; the necessary drop-in for AWS OpenSearch domains.&lt;/li&gt;
&lt;li&gt;Upgrading to &lt;code&gt;opensearch-py&lt;/code&gt; alone does not guarantee a security fix because its &lt;code&gt;urllib3&lt;/code&gt; constraint is a floor (&lt;code&gt;&amp;gt;=1.26.19&lt;/code&gt;), requiring an explicit application-level pin of &lt;code&gt;urllib3&amp;gt;=2.5.0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When third-party packages like &lt;code&gt;django-elasticsearch-dsl-drf&lt;/code&gt; are abandoned, vendoring a lean (~190 LOC) project-local filter and pagination layer prevents breaking client-facing REST API response contracts.&lt;/li&gt;
&lt;li&gt;Verifying a search client migration requires separating metadata ping traffic (&lt;code&gt;size=0&lt;/code&gt;) from document retrieval queries and verifying vulnerability remediation with automated security auditing tools.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When a security advisory hits a core library like &lt;code&gt;urllib3&lt;/code&gt;, upgrading is usually a routine dependency bump. However, when legacy search clients hard-cap underlying transport libraries, security remediations become full-scale client migrations.&lt;/p&gt;
&lt;p&gt;This is the story of that digging: how a security patch that looked like a one-line version bump turned into a full client-library migration, what part of that migration was genuinely mechanical, and what part wasn’t. If you’re running an older &lt;code&gt;elasticsearch&lt;/code&gt; Python client against an AWS OpenSearch domain, there’s a decent chance you’re sitting on the same trap.&lt;/p&gt;
&lt;h2 id=&quot;the-anatomy-of-a-transitive-cve-trap&quot;&gt;The anatomy of a transitive CVE trap&lt;a href=&quot;#the-anatomy-of-a-transitive-cve-trap&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: The anatomy of a transitive CVE trap&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In modern Python backend architectures, transitive dependencies can quietly turn into security bottlenecks. Our production Django application relied on &lt;code&gt;elasticsearch==7.10.1&lt;/code&gt; to interact with event and audit-log indices hosted on AWS OpenSearch 1.3.&lt;/p&gt;
&lt;p&gt;A transitive dependency is a package your code never imports directly, it gets pulled in because something you depend on needs it. That distance is exactly why a cap like this goes unnoticed for years: nobody on the team ever wrote &lt;code&gt;import urllib3&lt;/code&gt;, so nobody was watching its version.&lt;/p&gt;
&lt;p&gt;When &lt;strong&gt;CVE-2025-50181&lt;/strong&gt; (an open-redirect / SSRF vulnerability in &lt;code&gt;urllib3&lt;/code&gt;, affecting everything before 2.5.0, &lt;a href=&quot;https://github.com/advisories/GHSA-pq67-6m6q-mj2v&quot;&gt;details here&lt;/a&gt;) was disclosed, security compliance required updating &lt;code&gt;urllib3&lt;/code&gt; to version &lt;code&gt;2.5.0&lt;/code&gt; or higher. However, running a dependency audit revealed a hard blocker:&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;toml&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;toml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# pyproject.toml (legacy state)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;elasticsearch &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;7.10.1&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt; # Pinned: requires urllib3 &amp;gt;= 1.21.1, &amp;lt; 2&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;The &lt;code&gt;elasticsearch 7.10.1&lt;/code&gt; package explicitly capped &lt;code&gt;urllib3 &amp;lt; 2&lt;/code&gt;. Because &lt;code&gt;1.26.20&lt;/code&gt; was the final release on the &lt;code&gt;urllib3 1.26.x&lt;/code&gt; line, no backport was available. Upgrading &lt;code&gt;elasticsearch-py&lt;/code&gt; to &lt;code&gt;7.14+&lt;/code&gt; or &lt;code&gt;8.x&lt;/code&gt; was not an option due to the introduction of client-side product checks (&lt;code&gt;UnsupportedProductError&lt;/code&gt;), which intentionally refuse connection to AWS OpenSearch or Elasticsearch OSS endpoints.&lt;/p&gt;
&lt;p&gt;The client library itself was forcing the application to remain vulnerable. The only remediation path was swapping the underlying search client entirely.&lt;/p&gt;
&lt;h2 id=&quot;client-selection-why-opensearch-py-not-a-newer-elasticsearch-client&quot;&gt;Client selection: why &lt;code&gt;opensearch-py&lt;/code&gt;, not a newer Elasticsearch client&lt;a href=&quot;#client-selection-why-opensearch-py-not-a-newer-elasticsearch-client&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Client selection: why opensearch-py, not a newer Elasticsearch client&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;OpenSearch is AWS’s fork of Elasticsearch: a separately maintained, Apache-licensed search engine with (mostly) the same API, created specifically so companies could keep running an open-source-licensed alternative after Elastic changed its own license.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;elasticsearch==7.10.1&lt;/code&gt; pin predates a fight that had nothing to do with us. Elastic changed its licensing in January 2021 after AWS forked Elasticsearch into what became OpenSearch, and that July &lt;code&gt;elasticsearch-py&lt;/code&gt; 7.14.0 quietly added a check that raises &lt;code&gt;UnsupportedProductError&lt;/code&gt; on any server missing Elastic’s own &lt;code&gt;X-Elastic-Product&lt;/code&gt; header (&lt;a href=&quot;https://github.com/elastic/elasticsearch-py/issues/1639&quot;&gt;issue #1639&lt;/a&gt;). AWS OpenSearch doesn’t send it. Elastic called the change an enhancement; the backlash was loud enough that &lt;a href=&quot;https://www.theregister.com/2021/08/09/elasticsearch_python_client_change/&quot;&gt;Elastic locked the GitHub thread&lt;/a&gt;. Practically, it meant any &lt;code&gt;elasticsearch-py&lt;/code&gt; release from 7.14 onward refuses to talk to our production database, so upgrading the vulnerable client in place was never on the table.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;opensearch-py&lt;/code&gt; sidesteps that fight by construction: it’s OpenSearch’s own client, forked from &lt;code&gt;elasticsearch-py&lt;/code&gt; at the 7.10.x line before the check existed. It never inherited it, and from a recent enough release, it drops the urllib3 ceiling too. &lt;a href=&quot;https://github.com/opensearch-project/opensearch-py/blob/main/COMPATIBILITY.md&quot;&gt;&lt;code&gt;COMPATIBILITY.md&lt;/code&gt;&lt;/a&gt; confirms it supports OpenSearch 1.0 through 2.x, our production range, and since OpenSearch’s wire protocol is still the Elasticsearch 7.x REST API, the same client talks cleanly to both our production domain and the legacy &lt;code&gt;elasticsearch-oss:7.10.2&lt;/code&gt; container we ran in CI.&lt;/p&gt;
&lt;h2 id=&quot;picking-a-version-was-its-own-research-project&quot;&gt;Picking a version was its own research project&lt;a href=&quot;#picking-a-version-was-its-own-research-project&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Picking a version was its own research project&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Here’s the part that surprised me: even after deciding on &lt;code&gt;opensearch-py&lt;/code&gt;, picking &lt;em&gt;which&lt;/em&gt; version wasn’t obvious, and getting it wrong would have shipped a “fix” that didn’t actually fix anything.&lt;/p&gt;
&lt;p&gt;Not every &lt;code&gt;opensearch-py&lt;/code&gt; release frees urllib3. Early 1.x and 2.x releases inherited the exact same &lt;code&gt;&amp;lt;2&lt;/code&gt; cap we were trying to escape. One release briefly dropped the cap without formally declaring urllib3-2 support, and the very next release re-added it. The real cutover point was version 2.6.0, shipped in &lt;a href=&quot;https://github.com/opensearch-project/opensearch-py/pull/719&quot;&gt;PR #719&lt;/a&gt;:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;opensearch-py&lt;/th&gt;
&lt;th&gt;urllib3 constraint&lt;/th&gt;
&lt;th&gt;Frees urllib3?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1.x&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;gt;=1.21.1,&amp;lt;2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No, same cap as before&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2.0.0 through 2.3.1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;gt;=1.21.1,&amp;lt;2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2.4.x&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;gt;=1.26.18&lt;/code&gt; (cap dropped, undeclared)&lt;/td&gt;
&lt;td&gt;Unofficially, briefly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2.5.0&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;gt;=1.26.18,&amp;lt;2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No, cap re-added&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2.6.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;!=2.2.0,&amp;lt;3,&amp;gt;=1.26.18&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes, first real support&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2.8.0 (latest 2.x at the time)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;!=2.2.0,!=2.2.1,&amp;lt;3,&amp;gt;=1.26.19&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes, plus more fixes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3.0.0 and later&lt;/td&gt;
&lt;td&gt;same floor&lt;/td&gt;
&lt;td&gt;Yes, but enforces keyword-only arguments, a bigger diff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;We landed on 2.8.0: same migration effort as 2.6.0, but with roughly six more months of bug and security fixes, while still avoiding the breaking change in 3.0 (which makes arguments like &lt;code&gt;body=&lt;/code&gt; keyword-only, not removed, just no longer allowed positionally, which matters if any of your call sites pass it positionally).&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;In dependency terms, &lt;code&gt;&amp;gt;=1.26.19&lt;/code&gt; is a floor: it says “at least this version,” not “exactly this version.” Package maintainers almost always publish floors rather than pins, because a pin would make their library incompatible with anything else in your project that needs a different exact version. That’s exactly why a floor alone can’t guarantee what actually gets installed.&lt;/p&gt;
&lt;p&gt;The gotcha I’d flag loudest: that urllib3 constraint is a floor, not a pin. &lt;code&gt;1.26.19&lt;/code&gt; satisfies &lt;code&gt;&amp;gt;=1.26.19&lt;/code&gt; just as well as &lt;code&gt;2.7.0&lt;/code&gt; does, so installing &lt;code&gt;opensearch-py&lt;/code&gt; 2.8 doesn’t guarantee your resolver picks a patched urllib3, it just stops forbidding it. Skip a separate &lt;code&gt;urllib3&amp;gt;=2.5.0&lt;/code&gt; pin in your own dependency file, and you can finish this entire migration still shipping the vulnerable version, because the old version still satisfies every constraint in the graph. We pinned it explicitly, with a comment citing the CVE, so the reason survives whoever edits that line next.&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;toml&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;toml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# pyproject.toml&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;[tool.poetry.dependencies]&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;python &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;^3.12&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;opensearch-py &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;^2.8&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# Explicitly floor urllib3 &amp;gt;= 2.5.0 for CVE-2025-50181 remediation.&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# opensearch-py only floors at &amp;gt;=1.26.19, so explicit flooring forces the resolver to upgrade.&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;urllib3 &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;&amp;gt;=2.5.0, &amp;lt;3&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;h2 id=&quot;bridging-the-drf-gap-the-one-piece-of-code-we-actually-had-to-write&quot;&gt;Bridging the DRF gap: the one piece of code we actually had to write&lt;a href=&quot;#bridging-the-drf-gap-the-one-piece-of-code-we-actually-had-to-write&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Bridging the DRF gap: the one piece of code we actually had to write&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Most of the Python call-site changes are import renames (&lt;code&gt;from opensearchpy import OpenSearch, Q, Search&lt;/code&gt; in place of the Elasticsearch equivalents), pure find-and-replace. Django REST Framework integration was the exception: DRF’s ordering and pagination classes expect a Django ORM queryset, not an &lt;code&gt;elasticsearch-dsl&lt;/code&gt; &lt;code&gt;Search&lt;/code&gt; object, so our workflow changelog view relied on &lt;code&gt;django-elasticsearch-dsl-drf&lt;/code&gt; to bridge that gap.&lt;/p&gt;
&lt;p&gt;That package depends on the old &lt;code&gt;elasticsearch&lt;/code&gt; client, reintroducing the urllib3 cap, and hasn’t shipped since &lt;a href=&quot;https://pypi.org/project/django-elasticsearch-dsl-drf/&quot;&gt;July 2022&lt;/a&gt;, with no OpenSearch successor: &lt;a href=&quot;https://github.com/codoc-health/django-opensearch-dsl&quot;&gt;&lt;code&gt;django-opensearch-dsl&lt;/code&gt;&lt;/a&gt; only handles indexing, and &lt;code&gt;django-anysearch-dsl-drf&lt;/code&gt; never made it to PyPI.&lt;/p&gt;
&lt;p&gt;Rewriting the endpoint onto the ORM would have meant a real backfill, not a client swap, so we vendored the roughly 190 lines we actually used into a project-local module, &lt;code&gt;processor/v1/opensearch_drf.py&lt;/code&gt;. If you hit the same wall: check whether a maintained package already covers your usage (indexing-only ones often skip the DRF layer), how narrow your actual usage is, and whether anything downstream depends on an exact response shape.&lt;/p&gt;
&lt;h3 id=&quot;preserving-the-api-contract&quot;&gt;Preserving the API contract&lt;a href=&quot;#preserving-the-api-contract&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Preserving the API contract&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The frontend application depended on a rigid JSON response structure for pagination and ordering:&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;json&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;{&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;  &quot;count&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; 142&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;  &quot;facets&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; {}&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;  &quot;next&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;https://api.domain.com/v1/.../changelog/events/?page=2&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;  &quot;previous&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; null&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;  &quot;results&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; [...]&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;To maintain 100% backward compatibility without changing frontend integration code, our inline &lt;code&gt;PageNumberPagination&lt;/code&gt; implementation handles OpenSearch’s response structure explicitly:&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;python&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# processor/v1/opensearch_drf.py (excerpt)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; rest_framework&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;pagination &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; PageNumberPagination &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; DRFPageNumberPagination&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; rest_framework&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;response &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; Response&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; rest_framework&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;exceptions &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; NotFound&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; PageNumberPagination&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;DRFPageNumberPagination&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;):&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;    &quot;&quot;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;    Project-local DRF paginator over an opensearchpy Search object.&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;    Preserves exact contract: {count, facets, next, previous, results}.&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;    &quot;&quot;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    page_size &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; 50&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;  # Default PAGE_SIZE&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; paginate_queryset&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt; queryset&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt; request&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt; view&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;):&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;        page_number &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; request&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;query_params&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;get&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(self.page_query_param, &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;        # Calculate search slice&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        try&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;            page_number &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; int&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(page_number)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        except&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; ValueError&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;            if&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; page_number &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &apos;last&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;                page_number &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;_get_last_page_number&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(queryset)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;            else&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;                raise&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; NotFound&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;Invalid page.&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;        bottom &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; (page_number &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;page_size&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;        top &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; bottom &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;page_size&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;        # Execute sliced opensearchpy Search query&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;        self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;response &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; queryset&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;bottom&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;top&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;].&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;execute&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;()&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;        # Parse total hits (supports ES7+ / OpenSearch dict/object shapes)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;        total &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;response&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;hits&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;total&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;        self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;count &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; total&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;value &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; hasattr&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(total, &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&apos;value&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; else&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; total&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;count &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; and&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; bottom &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;&amp;gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;count&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;            raise&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; NotFound&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;Invalid page.&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;        self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;request &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; request&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;        self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;page_number &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; page_number&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; list&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(self.response)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; get_paginated_response&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt; data&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;):&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; Response&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;({&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;            &apos;count&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;: self.count,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;            &apos;facets&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;: {},  &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# Maintained for strict frontend schema contract&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;            &apos;next&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;: self.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;get_next_link&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(),&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;            &apos;previous&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;: self.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;get_previous_link&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(),&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;            &apos;results&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;: data&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;        })&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;By coupling this with an &lt;code&gt;OrderingFilterBackend&lt;/code&gt; that transforms user query parameters (&lt;code&gt;?ordering=-timestamp&lt;/code&gt;) into OpenSearch &lt;code&gt;.sort({&apos;timestamp&apos;: {&apos;order&apos;: &apos;desc&apos;}})&lt;/code&gt; execution calls, we dropped &lt;code&gt;django-elasticsearch-dsl-drf&lt;/code&gt; completely and uninstalled four transitive packages.&lt;/p&gt;
&lt;h2 id=&quot;two-gotchas-worth-knowing-before-you-try-this&quot;&gt;Two gotchas worth knowing before you try this&lt;a href=&quot;#two-gotchas-worth-knowing-before-you-try-this&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Two gotchas worth knowing before you try this&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;An app-name collision broke an import at boot.&lt;/strong&gt; &lt;code&gt;opensearch-py&lt;/code&gt;’s optional metrics module does &lt;code&gt;from events import Events&lt;/code&gt;, expecting a small PyPI package called &lt;em&gt;Events&lt;/em&gt;. Our own top-level Django app was also named &lt;code&gt;events&lt;/code&gt;, and it shadowed the real package. The fix was a two-line shim satisfying the import, not a working metrics backend, just enough to stop the crash, since we don’t use that feature anyway.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;“No product check” is the point, not a risk to route around.&lt;/strong&gt; It’s the entire reason one client works against both a legacy Elasticsearch-OSS container in CI and a real AWS OpenSearch domain in production. If you ever see &lt;code&gt;UnsupportedProductError&lt;/code&gt; after this migration, it means a stray &lt;code&gt;import elasticsearch&lt;/code&gt; survived somewhere. A full-tree grep should come back completely clean.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;verification--observability-strategy&quot;&gt;Verification &amp;amp; observability strategy&lt;a href=&quot;#verification--observability-strategy&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Verification &amp;amp; observability strategy&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Validating search client migrations requires separating metadata aggregations (&lt;code&gt;size=0&lt;/code&gt;) from real document retrieval hits.&lt;/p&gt;
&lt;p&gt;In Datadog, we isolated event retrieval traffic using query exclusions:&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;service:backend-api @logger.name:opensearch -&quot;size=0&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;A successful status &lt;code&gt;200&lt;/code&gt; response on a non-&lt;code&gt;size=0&lt;/code&gt; query confirmed that both ordering backends, pagination slicing, and document mapping round-tripped cleanly through &lt;code&gt;opensearch-py&lt;/code&gt; to AWS OpenSearch.&lt;/p&gt;
&lt;p&gt;Finally, running &lt;code&gt;pip-audit&lt;/code&gt; verified the security milestone:&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;bash&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; pip-audit&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;No&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; known&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; vulnerabilities&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; found&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
</content:encoded><category>python</category><category>django</category><category>opensearch</category><category>security</category></item><item><title>Almost done: the hidden tradeoffs of Playwright E2E testing in a fast-moving app</title><link>https://serhiisoldatov.dev/blog/hidden-tradeoffs-of-playwright-e2e-testing/</link><guid isPermaLink="true">https://serhiisoldatov.dev/blog/hidden-tradeoffs-of-playwright-e2e-testing/</guid><description>A week of investigating Playwright on a fast-moving UI ended with a recommendation to stop. Four hidden costs that make end-to-end tests cheap to start and expensive to keep.</description><pubDate>Sun, 07 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I kept thinking I was one fix away from done. Add a stable identifier here, patch a selector there, and the suite would finally go green. Then green would reveal the next thing underneath it. By the end of the week I’d learned something I hadn’t known going in: this is just how end-to-end testing behaves on a product whose UI changes every release. My planning was never the issue.&lt;/p&gt;
&lt;p&gt;The premise was almost too clean. The goal was modest: a test that clicks through creating a campaign the way a real user would, so we’d know the moment a merge broke it. You record that flow once with Playwright’s codegen, paste the raw output into a Claude skill, and get back a structured test. No writing selectors by hand, no describing every button. Record, convert, done. I built that workflow, and it worked: the first test went green. That’s the dangerous part — the surface really does deliver, which is exactly why you trust it before you’ve seen what’s underneath.&lt;/p&gt;
&lt;h2 id=&quot;the-first-cost-the-app-has-to-be-built-to-be-recorded&quot;&gt;The first cost: the app has to be built to be recorded&lt;a href=&quot;#the-first-cost-the-app-has-to-be-built-to-be-recorded&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: The first cost: the app has to be built to be recorded&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Codegen can only record what the page makes addressable. Point it at a button with visible text and you get a clean selector. Point it at a three-dot actions menu with no label, and you get &lt;code&gt;getByRole(&apos;button&apos;).nth(5)&lt;/code&gt;, an index that breaks the instant a row reorders. Point it at one of our react-select dropdowns and codegen records &lt;code&gt;.emotion-class-nxiuxh-container &amp;gt; … &amp;gt; .emotion-class-11rjtvl&lt;/code&gt;, a chain of generated CSS classes that won’t survive the next restyle.&lt;/p&gt;
&lt;p&gt;So before you record anything, every interactive element in the flow needs a stable identifier added to the source. Not all of them had one. That meant an audit-and-prep pass through the components first, and another pass every time a new flow gets added later. The “record” step everyone pictures as step one is actually step two.&lt;/p&gt;
&lt;h2 id=&quot;the-second-cost-conversion-is-its-own-negotiation&quot;&gt;The second cost: conversion is its own negotiation&lt;a href=&quot;#the-second-cost-conversion-is-its-own-negotiation&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: The second cost: conversion is its own negotiation&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Say you’ve prepped the app and recorded the flow. The conversion still isn’t one-click. The skill turns most of the recording into a clean test, then stops at the parts it can’t resolve on its own: a selector it doesn’t recognize, an element that’s ambiguous, a dropdown that still came through as an emotion class. For each one it asks a question, and answering it means going back into the source: add an &lt;code&gt;inputId&lt;/code&gt; here, a &lt;code&gt;data-testid&lt;/code&gt; there, re-record or hand-patch. A recording is just a list of clicks. The real work is reconstructing the intent behind them, and that work lives half in the test file and half back in the components.&lt;/p&gt;
&lt;p&gt;And that’s the cost for a short flow. The flows actually worth testing aren’t short.&lt;/p&gt;
&lt;h2 id=&quot;the-third-cost-the-most-valuable-tests-are-the-most-fragile&quot;&gt;The third cost: the most valuable tests are the most fragile&lt;a href=&quot;#the-third-cost-the-most-valuable-tests-are-the-most-fragile&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: The third cost: the most valuable tests are the most fragile&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The whole point of end-to-end testing is the long flow — create a campaign, then a line item inside it, then a visual on top of that. Fifty to a hundred interactive elements in a single chain, the exact journey a real user takes and the exact journey no unit test can cover. That’s where the value is. It’s also where the fragility is, and it’s the same property producing both: the test is long because the flow is long, and every step in it is something that can change. A relabeled field in the middle, a reordered step, a drawer that now opens differently — any one of them breaks the chain from that point on, and a single UI tweak can turn into hours of tracing which step actually moved. The Page Object Model helps here: fix the selector in one class and every test that uses it recovers. But it only softens the blow, it doesn’t stop the punches. The flows you most want to protect are the ones that break most often.&lt;/p&gt;
&lt;h2 id=&quot;the-fourth-cost-the-test-data-you-cant-take-back&quot;&gt;The fourth cost: the test data you can’t take back&lt;a href=&quot;#the-fourth-cost-the-test-data-you-cant-take-back&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: The fourth cost: the test data you can’t take back&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The first three costs are all about getting a test written and keeping it green. The fourth is different in kind. It’s about what a test leaves behind. A real campaign flow doesn’t just touch our own database; it creates entities on TikTok and Facebook. Internal records you can clean up: record the ID, fire a delete in &lt;code&gt;afterEach&lt;/code&gt;, and a nightly cron sweeps whatever a crashed run left orphaned. External entities you can’t, at least not reliably. Each platform has its own deletion rules, its own delays, its own idea of what’s even allowed. There’s no scheduled job that dependably undoes them. And unlike the other three, this cost doesn’t shrink as the suite matures. It’s a standing risk that grows with every test that touches a real platform.&lt;/p&gt;
&lt;h2 id=&quot;the-cost-doesnt-end--thats-the-finding&quot;&gt;The cost doesn’t end — that’s the finding&lt;a href=&quot;#the-cost-doesnt-end--thats-the-finding&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: The cost doesn’t end — that’s the finding&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Here’s what ties the four together. A normal feature has a build cost, and then it’s done. These costs don’t behave that way. The prep, the conversion fixups, the broken chains, the orphaned external data — each one recurs every time the UI moves, and on this product the UI moves every release. You don’t build the suite once and walk away. You keep paying for it, and the bill scales with exactly the velocity that makes the product worth testing.&lt;/p&gt;
&lt;p&gt;That’s why I stopped thinking I was one fix away from done. There was no “finish” to reach at the cost I’d assumed; I just hadn’t seen that yet. So the recommendation I brought back wasn’t a test suite. It was: pause E2E investment for now, and keep the groundwork in place — the Page Object structure, the recording workflow, the selector conventions, the conversion skill all stay, ready if we ever decide the trade is worth it. The investigation produced something better than the thing we set out to build: a clear-eyed reason not to build it yet, and that turned out to be worth more than the suite would have been.&lt;/p&gt;
&lt;h2 id=&quot;if-youre-about-to-add-playwright-to-your-project&quot;&gt;If you’re about to add Playwright to your project&lt;a href=&quot;#if-youre-about-to-add-playwright-to-your-project&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: If you’re about to add Playwright to your project&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Here’s what to actually expect, in plain terms:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you’re signing up for&lt;/th&gt;
&lt;th&gt;When it costs you&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Prep before you can record.&lt;/strong&gt; Every element in a flow needs a stable identifier in the source first.&lt;/td&gt;
&lt;td&gt;One upfront audit per app, then a smaller pass &lt;em&gt;every time you add a new flow&lt;/em&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cleanup after recording.&lt;/strong&gt; The auto-conversion gets you most of the way; the rest is manual fixes back in the source.&lt;/td&gt;
&lt;td&gt;A chunk of time &lt;em&gt;per flow&lt;/em&gt;, every flow. It never drops to zero.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maintaining long flows.&lt;/strong&gt; The high-value multi-step tests break whenever a step in the middle changes.&lt;/td&gt;
&lt;td&gt;Recurring, &lt;em&gt;per UI change&lt;/em&gt;. A single tweak can cost hours of tracing.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;External test data.&lt;/strong&gt; Entities created on TikTok/Facebook can’t be reliably auto-deleted.&lt;/td&gt;
&lt;td&gt;Ongoing risk that never resolves; it grows with every test that touches a real platform.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;None of these are one-time. The setup is the part you can budget for; the rest you pay on every release.&lt;/p&gt;
</content:encoded><category>playwright</category><category>e2e-testing</category><category>engineering-judgment</category></item><item><title>How I added project-aware tab completion to Django&apos;s manage.py</title><link>https://serhiisoldatov.dev/blog/django-manage-py-tab-completion/</link><guid isPermaLink="true">https://serhiisoldatov.dev/blog/django-manage-py-tab-completion/</guid><description>Django&apos;s bundled bash completion only knows command names and flags. django-completion adds app labels and migration names by reading a JSON cache that is rebuilt after each manage.py command, so Tab never loads Django.</description><pubDate>Thu, 21 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Django technically ships with a bash completion script — it lives in &lt;code&gt;extras/django_bash_completion&lt;/code&gt;. But nothing wires it up when you &lt;code&gt;pip install django&lt;/code&gt;. You have to find it, source it manually, and once you do, it completes command names and flags. That’s it.&lt;/p&gt;
&lt;p&gt;App labels? Migration names? No.&lt;/p&gt;
&lt;p&gt;This has been an open issue since &lt;a href=&quot;https://code.djangoproject.com/ticket/1240&quot;&gt;ticket #1240&lt;/a&gt;, filed in 2006.
The discussion already identified app-name completion as the tricky part.
Twenty years later it still isn’t in core.&lt;/p&gt;
&lt;p&gt;I built &lt;a href=&quot;https://github.com/soldatov-ss/django-completion&quot;&gt;django-completion&lt;/a&gt; to fix it. This article covers that.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-daily-friction&quot;&gt;The daily friction&lt;a href=&quot;#the-daily-friction&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: The daily friction&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The thing that bothered me wasn’t forgetting command names. It was the two steps right after:&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;bash&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;python&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; manage.py&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; migrate&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; acc&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt;Ente&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;r&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;           # too early — typo&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;python&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; manage.py&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; migrate&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; accounts&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; 0003&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt;Ta&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;b&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;  # what&apos;s the exact filename?&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;Guessing migration names is the worst. They follow a pattern
(&lt;code&gt;0003_add_user_profile&lt;/code&gt;) but you have to check the filesystem or run &lt;code&gt;showmigrations&lt;/code&gt; first. Either way, you’ve left the flow.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;why-argparse-completion-doesnt-reach-this&quot;&gt;Why argparse completion doesn’t reach this&lt;a href=&quot;#why-argparse-completion-doesnt-reach-this&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Why argparse completion doesn’t reach this&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Tools like &lt;a href=&quot;https://github.com/kislyuk/argcomplete&quot;&gt;argcomplete&lt;/a&gt; or Django’s own &lt;code&gt;django_bash_completion&lt;/code&gt; parse the command’s argparse definition and surface flags: &lt;code&gt;--verbosity&lt;/code&gt;, &lt;code&gt;--settings&lt;/code&gt;, &lt;code&gt;--no-input&lt;/code&gt;, etc.&lt;/p&gt;
&lt;p&gt;What they can’t surface is positional arguments that come from &lt;em&gt;your project&lt;/em&gt; — app labels, migration names, test module paths. Those aren’t declared in argparse. They’re discovered at runtime by loading &lt;code&gt;DJANGO_SETTINGS_MODULE&lt;/code&gt; and calling &lt;code&gt;django.setup()&lt;/code&gt;, which initializes the full app registry.&lt;/p&gt;
&lt;p&gt;Doing that on every keypress is the wrong tradeoff. On a medium-sized project, &lt;code&gt;django.setup()&lt;/code&gt; takes 200–500ms. That’s noticeable as input lag, and the approach doesn’t compose well with virtual environments or &lt;code&gt;uv&lt;/code&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-django-completion-does-differently&quot;&gt;What django-completion does differently&lt;a href=&quot;#what-django-completion-does-differently&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: What django-completion does differently&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Terminal demo of manage.py tab completion suggesting commands, app labels, and migration names&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-1.gif&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;Instead of loading Django at completion time, the approach is inverted:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Discovery happens after commands run, not when Tab is pressed.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;After any &lt;code&gt;manage.py&lt;/code&gt; command finishes, django-completion rebuilds a small JSON cache at your project root. Tab completion reads that file. No Django import, no database access, no perceptible delay.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;install&quot;&gt;Install&lt;a href=&quot;#install&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Install&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;bash&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;pip&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; install&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; django-completion&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;Add to &lt;code&gt;INSTALLED_APPS&lt;/code&gt;:&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;python&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;INSTALLED_APPS &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; [&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;    # ...&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;django_completion&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;Run once to wire up the shell script:&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;bash&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;python&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; manage.py&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; autocomplete&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; install&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;Restart your terminal. Tab works.&lt;/p&gt;
&lt;p&gt;All common invocation forms are covered:&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;bash&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;manage.py&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; migrate&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt;TA&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;B&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;./manage.py&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; migrate&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt;TA&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;B&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;python&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; manage.py&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; migrate&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt;TA&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;B&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;python3&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; manage.py&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; migrate&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt;TA&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;B&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;uv&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; run&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; python&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; manage.py&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; migrate&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt;TA&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;B&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;hr&gt;
&lt;h2 id=&quot;how-it-works&quot;&gt;How it works&lt;a href=&quot;#how-it-works&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: How it works&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;After any management command finishes, &lt;code&gt;AppConfig.ready()&lt;/code&gt; has patched &lt;code&gt;BaseCommand.execute&lt;/code&gt; to spawn a background thread that rebuilds the cache if it’s stale. The patch looks like this:&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;python&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; patched&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt;cmd_self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt;args&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; **&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt;kwargs&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;):&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    try&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; original_execute&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(cmd_self, &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;args, &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;**&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;kwargs)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    finally&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;        thread &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; threading&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;Thread&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(target&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;maybe_refresh_cache)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;        thread&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;start&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;The thread writes &lt;code&gt;.django-completion-cache.json&lt;/code&gt; to &lt;code&gt;BASE_DIR&lt;/code&gt; — command names, app labels, per-command flags, migration filenames. A 60-second cooldown prevents redundant rebuilds when you run several commands in a row. The cache refresh doesn’t block your command at all.&lt;/p&gt;
&lt;p&gt;When you press Tab, the shell reads that file and passes the current line to a small Python helper that applies a declarative rule table:&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;python&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;_COMMAND_RULES &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;migrate&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;        CommandRule&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(pos2&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;migration_apps&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,  pos3&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;migration_names&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;),&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;showmigrations&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; CommandRule&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(pos2&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;migration_apps&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,  pos3&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;options&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;),&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;makemigrations&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; CommandRule&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(pos2&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;local_apps&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,      pos3&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;options&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;),&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;sqlmigrate&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;     CommandRule&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(pos2&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;migration_apps&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,  pos3&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;migration_names_no_zero&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;),&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;dumpdata&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;       CommandRule&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(pos2&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;all_apps&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,        pos3&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;options&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;),&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;test&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;           CommandRule&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(pos2&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;all_apps&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,        pos3&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;options&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;),&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;&lt;code&gt;migrate&lt;/code&gt; at position 2 gets app labels filtered to those that have migrations. At position 3 it gets migration names for the app already typed, plus &lt;code&gt;zero&lt;/code&gt;. A word starting with &lt;code&gt;-&lt;/code&gt; always triggers flag completion. Commands not in the table fall back to flags only — a safe default for custom management commands.&lt;/p&gt;
&lt;p&gt;Two smaller details worth naming: the shell script walks up from &lt;code&gt;$PWD&lt;/code&gt; to find the cache file, so completion works from any subdirectory in your project. And &lt;code&gt;makemigrations&lt;/code&gt; only surfaces &lt;em&gt;local&lt;/em&gt; apps — ones inside &lt;code&gt;BASE_DIR&lt;/code&gt; — so &lt;code&gt;django.contrib.admin&lt;/code&gt; and friends don’t clutter the list.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;whats-next&quot;&gt;What’s next&lt;a href=&quot;#whats-next&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: What’s next&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fish shell&lt;/strong&gt; — the completion API is different from bash/zsh; the cache format is stable if anyone wants to write a Fish module&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PowerShell&lt;/strong&gt; — out of scope for me personally, same offer&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fuzzy matching&lt;/strong&gt; — there’s a stub in the codebase; not shipped yet&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;try-it&quot;&gt;Try it&lt;a href=&quot;#try-it&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Try it&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;bash&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;pip&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; install&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; django-completion&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href=&quot;https://github.com/soldatov-ss/django-completion&quot;&gt;github.com/soldatov-ss/django-completion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href=&quot;https://soldatov-ss.github.io/django-completion&quot;&gt;soldatov-ss.github.io/django-completion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;PyPI: &lt;a href=&quot;https://pypi.org/project/django-completion&quot;&gt;pypi.org/project/django-completion&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If it breaks on your setup — unusual &lt;code&gt;BASE_DIR&lt;/code&gt;, custom &lt;code&gt;MIGRATION_MODULES&lt;/code&gt;, monorepo layout — open an issue. Those edge cases are the interesting ones.&lt;/p&gt;
&lt;p&gt;If it works, a ⭐ on GitHub helps other Django developers find it.&lt;/p&gt;
</content:encoded><category>django</category><category>python</category><category>shell</category><category>developer-tooling</category></item><item><title>From Django library to AWS Cognito: my journey building enterprise SSO</title><link>https://serhiisoldatov.dev/blog/django-enterprise-sso-with-aws-cognito/</link><guid isPermaLink="true">https://serhiisoldatov.dev/blog/django-enterprise-sso-with-aws-cognito/</guid><description>How a multi-provider SSO requirement took me from rewriting a Django SSO library to an AWS Cognito setup where adding a new identity provider needs no server code changes.</description><pubDate>Sun, 12 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;the-client-call-that-started-everything&quot;&gt;The client call that started everything&lt;a href=&quot;#the-client-call-that-started-everything&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: The client call that started everything&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;“We need SSO. Multiple providers. Per client. Starting next sprint.”&lt;/p&gt;
&lt;p&gt;That was the moment I realized I had no idea what I was actually getting into.&lt;/p&gt;
&lt;p&gt;I’d built auth flows before — JWT, session-based, OAuth2 callbacks. But enterprise SSO at scale, where each tenant brings their own identity provider, where role sync needs to be automatic, where a user getting removed from an Azure group means they lose access in your app within minutes? That’s a different animal entirely.&lt;/p&gt;
&lt;p&gt;This is the story of how I went from frantically Googling “Django SSO library” to building a production system on AWS Cognito that handles Azure AD, Okta, and any OIDC provider — without touching a line of server code when a new provider is added.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;v1-wrestling-a-library-into-submission&quot;&gt;V1: wrestling a library into submission&lt;a href=&quot;#v1-wrestling-a-library-into-submission&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: V1: wrestling a library into submission&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;h3 id=&quot;the-starting-point&quot;&gt;The starting point&lt;a href=&quot;#the-starting-point&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: The starting point&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;My first search led me to &lt;code&gt;django-simple-sso&lt;/code&gt; — the only SSO-focused library I could find for Django at the time. The bones were reasonable. The problem? It was designed for one provider, one configuration, one-to-one everything.&lt;/p&gt;
&lt;p&gt;My requirements were the opposite:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Multiple providers per client (a client might have Azure AD &lt;em&gt;and&lt;/em&gt; Okta)&lt;/li&gt;
&lt;li&gt;Dynamic provider selection by email domain&lt;/li&gt;
&lt;li&gt;Automatic role mapping: &lt;code&gt;admin&lt;/code&gt;, &lt;code&gt;manager&lt;/code&gt;, etc., synced from the provider&lt;/li&gt;
&lt;li&gt;Per-tenant configuration, entirely isolated&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;the-rearchitecture&quot;&gt;The rearchitecture&lt;a href=&quot;#the-rearchitecture&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: The rearchitecture&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;So I stopped trying to configure the library and started reading its internals. Then I started rewriting them.&lt;/p&gt;
&lt;p&gt;The data model went from one-to-one relationships to many-to-many. The auth flow got rewritten for dynamic provider resolution — when a user types their email, the backend queries which SSO configs are associated with that domain, selects the right provider, and redirects accordingly. Role mapping got its own layer.&lt;/p&gt;
&lt;p&gt;Three weeks of heads-down work. By the end, it covered everything: multiple providers, automatic role sync, dynamic authentication. It was custom, it was complex, and it worked.&lt;/p&gt;
&lt;p&gt;Until the next client call.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-problem-that-forced-v2&quot;&gt;The problem that forced V2&lt;a href=&quot;#the-problem-that-forced-v2&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: The problem that forced V2&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;h3 id=&quot;the-request&quot;&gt;The request&lt;a href=&quot;#the-request&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: The request&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;“We need to add a new provider.”&lt;/p&gt;
&lt;p&gt;Fine. I’d done it before. Except this one was an obscure, legacy enterprise identity system — the kind that’s only used in certain American B2B markets, where getting test credentials means purchasing a suite of products and weeks of vendor coordination. We literally could not test against it without significant time and cost investment.&lt;/p&gt;
&lt;p&gt;And that’s when the real problem became obvious: every new provider required code changes. Test access. Adjustment cycles. The library abstraction I’d built was still fundamentally tied to providers I could understand and validate myself.&lt;/p&gt;
&lt;p&gt;The client’s roadmap had more providers coming. We needed something universal.&lt;/p&gt;
&lt;h3 id=&quot;the-decision&quot;&gt;The decision&lt;a href=&quot;#the-decision&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: The decision&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;We removed everything. The library, the custom SSO code, all of it.&lt;/p&gt;
&lt;p&gt;And we moved to &lt;strong&gt;AWS Cognito&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;v2-why-cognito-changed-everything&quot;&gt;V2: why Cognito changed everything&lt;a href=&quot;#v2-why-cognito-changed-everything&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: V2: why Cognito changed everything&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;AWS Cognito sits between your application and any external identity provider. It speaks OIDC to the providers (Azure AD, Okta, Google, SAML, whatever) and speaks a single, consistent OAuth2 flow back to your app.&lt;/p&gt;
&lt;p&gt;The transformation this unlocked:&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;Before (V1):&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;App &amp;lt;--&amp;gt; Custom SSO code &amp;lt;--&amp;gt; [Azure-specific logic] &amp;lt;--&amp;gt; Azure AD&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;                          &amp;lt;--&amp;gt; [Okta-specific logic]  &amp;lt;--&amp;gt; Okta&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;                          &amp;lt;--&amp;gt; [???-specific logic]   &amp;lt;--&amp;gt; Unknown provider&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;After (V2):&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;App &amp;lt;--&amp;gt; Cognito &amp;lt;--&amp;gt; Azure AD&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;                &amp;lt;--&amp;gt; Okta&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;                &amp;lt;--&amp;gt; Any OIDC provider&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;Adding a new provider now requires zero server-side code changes.&lt;/strong&gt; Cognito normalises everything. The token your app receives is identical regardless of which provider the user authenticated against.&lt;/p&gt;
&lt;h3 id=&quot;what-we-built&quot;&gt;What we built&lt;a href=&quot;#what-we-built&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: What we built&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The Django application I’m open-sourcing handles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dynamic config discovery&lt;/strong&gt; — &lt;code&gt;GET /api/oidc/check_config_exists/&amp;lt;email&amp;gt;/&lt;/code&gt; returns the right Cognito client config for a given email domain&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OAuth2 callback&lt;/strong&gt; — exchanges the authorization code, decodes the ID token, maps roles, creates/updates the user and their membership record&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automated Cognito provisioning&lt;/strong&gt; — clients send their identity provider credentials via a web form; the backend calls AWS APIs to configure the Cognito User Pool, no manual AWS console work required&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lifecycle events via webhooks&lt;/strong&gt; — Azure AD and Okta can notify the app when a user is removed from a group or deleted. The system soft-deletes their membership, revoking access automatically&lt;/li&gt;
&lt;/ul&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;Browser          Django              Cognito          Azure AD / Okta&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;  |                 |                   |                   |&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;  |-- check email -&amp;gt;|                   |                   |&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;  |&amp;lt;- client_id  ---|                   |                   |&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;  |                 |                   |                   |&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;  |-------- redirect to Cognito -------&amp;gt;|                   |&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;  |                                     |--- OIDC flow ----&amp;gt;|&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;  |                                     |&amp;lt;-- user token ----|&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;  |&amp;lt;-------- redirect with ?code= ------|                   |&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;  |                 |                   |                   |&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;  |-- /callback  --&amp;gt;|                   |                   |&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;  |                 |-- exchange code -&amp;gt;|                   |&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;  |                 |&amp;lt;-- id_token ------|                   |&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;  |                 | decode + map roles                    |&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;  |                 | create/update User + Member           |&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;  |&amp;lt;- access_token--|                   |                   |&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-article-series&quot;&gt;The article series&lt;a href=&quot;#the-article-series&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: The article series&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This repo comes with a full series of step-by-step guides. Think of them as the manual that I wish had existed when I started:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;/blog/aws-cognito-sso-setup/&quot;&gt;1. How to Configure AWS Cognito for SSO: A Step-by-Step Guide&lt;/a&gt;&lt;/strong&gt;
The foundation. User Pool setup, custom attributes (&lt;code&gt;custom:groups&lt;/code&gt;, &lt;code&gt;custom:roles&lt;/code&gt;), app client configuration, and the callback URL wiring. Start here.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;/blog/azure-sso-setup/&quot;&gt;2. Azure Single Sign-On (SSO) Setup: A Step-by-Step Guide&lt;/a&gt;&lt;/strong&gt;
Enterprise App registration in Azure AD, configuring OIDC claims, mapping Azure groups to Cognito attributes. Includes the Graph API setup for lifecycle webhooks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;/blog/okta-sso-setup/&quot;&gt;3. Okta Single Sign-On (SSO) Setup: A Step-by-Step Guide&lt;/a&gt;&lt;/strong&gt;
Okta application setup, OIDC configuration, group claims, and the Event Hook configuration for real-time deprovisioning.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;/blog/sso-for-your-admin-panel/&quot;&gt;4. How to Set Up SSO Authentication in Your App’s Admin Panel&lt;/a&gt;&lt;/strong&gt;
The Django side: the authentication backend, the callback handler, role mapping, and how the admin panel reflects SSO-managed memberships.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;/blog/testing-sso-with-cognito-and-oidc/&quot;&gt;5. How to Test SSO Authentication with Cognito and OIDC Providers&lt;/a&gt;&lt;/strong&gt;
End-to-end testing strategy — what to mock, what to hit for real, and the test patterns used in this repo.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-repository&quot;&gt;The repository&lt;a href=&quot;#the-repository&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: The repository&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The code is on GitHub: &lt;strong&gt;&lt;a href=&quot;https://github.com/soldatov-ss/django-cognito-sso-demo&quot;&gt;django-cognito-sso-demo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It’s a reference implementation, not a drop-in package. The parts you’ll want to adapt for your own system:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;apps/oidc_auth/services/sso.py&lt;/code&gt;&lt;/strong&gt; — core SSO orchestration: config lookup, token exchange, role mapping&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;apps/oidc_auth/backends.py&lt;/code&gt;&lt;/strong&gt; — Django authentication backend wired to Cognito&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;apps/users/cognito.py&lt;/code&gt;&lt;/strong&gt; — boto3 wrapper for all Cognito API calls&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;apps/oidc_auth/services/notification.py&lt;/code&gt;&lt;/strong&gt; — webhook processing for Azure and Okta lifecycle events&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One intentional simplification worth noting: &lt;code&gt;RightsHolderService.get_rights_holder_from_request&lt;/code&gt; resolves the tenant from the request user directly. In production, tenant resolution typically comes from middleware or a partnership context. Replace it with whatever your multi-tenancy layer provides.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-id-tell-past-me&quot;&gt;What I’d tell past me&lt;a href=&quot;#what-id-tell-past-me&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: What I’d tell past me&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Don’t start with a library.&lt;/strong&gt; Or rather: start with one to understand the problem space, but be honest about when you’ve outgrown it. The three weeks I spent rearchitecting &lt;code&gt;django-simple-sso&lt;/code&gt; taught me exactly what I needed to know to make Cognito work well — I don’t regret the time, but I wish I’d recognised sooner that the constraint wasn’t the library, it was the model.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The “universal provider” requirement is the forcing function.&lt;/strong&gt; If you know upfront that you need to support arbitrary OIDC providers without code changes, skip straight to an identity broker. Cognito is the obvious choice in AWS environments. There are others (Auth0, Keycloak). The architecture is the same.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lifecycle events are not optional for enterprise.&lt;/strong&gt; User deprovisioning is a security requirement, not a nice-to-have. Budget for the webhook infrastructure and the soft-deletion logic early. It’s much harder to retrofit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Encrypt your client secrets.&lt;/strong&gt; &lt;code&gt;SSOAccountConfig&lt;/code&gt; uses &lt;code&gt;django-encrypted-model-fields&lt;/code&gt; for &lt;code&gt;client_id&lt;/code&gt; and &lt;code&gt;client_secret&lt;/code&gt;. If you’re storing provider credentials in your database — and you will be — this is non-negotiable.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;where-this-goes-next&quot;&gt;Where this goes next&lt;a href=&quot;#where-this-goes-next&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Where this goes next&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The system in this repo handles everything the original client needed. The architecture has stayed stable through adding new providers, new tenants, and changes in the underlying identity providers themselves.&lt;/p&gt;
&lt;p&gt;If you’re building something similar and hit a wall, the article series has the detail. The repo has the code.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Source code: &lt;a href=&quot;https://github.com/soldatov-ss/django-cognito-sso-demo&quot;&gt;github.com/your-username/django-cognito-sso-demo&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</content:encoded><category>django</category><category>aws</category><category>cognito</category><category>sso</category></item><item><title>How to set up SSO authentication in your app&apos;s admin panel</title><link>https://serhiisoldatov.dev/blog/sso-for-your-admin-panel/</link><guid isPermaLink="true">https://serhiisoldatov.dev/blog/sso-for-your-admin-panel/</guid><description>How to configure SSO account and notification settings in a Django admin panel backed by AWS Cognito, automate the setup through an admin API, and trigger user soft-deletes from Azure and Okta.</description><pubDate>Sun, 12 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;prerequisites-for-all-oidc-providers&quot;&gt;Prerequisites for all OIDC providers&lt;a href=&quot;#prerequisites-for-all-oidc-providers&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Prerequisites for all OIDC providers&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This guide covers the admin setup for SSO authentication built with Django and AWS Cognito. The full reference implementation is available on GitHub: &lt;a href=&quot;https://github.com/soldatov-ss/django-cognito-sso-demo&quot;&gt;django-cognito-sso-demo&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;how-sso-works-in-this-setup&quot;&gt;How SSO works in this setup&lt;a href=&quot;#how-sso-works-in-this-setup&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: How SSO works in this setup&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;The user authenticates via an external OIDC provider (e.g., Azure or Okta)&lt;/li&gt;
&lt;li&gt;AWS Cognito federates the identity&lt;/li&gt;
&lt;li&gt;Your application validates:
&lt;ul&gt;
&lt;li&gt;group membership&lt;/li&gt;
&lt;li&gt;email domain&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Access is granted or denied&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&quot;requirements&quot;&gt;Requirements&lt;a href=&quot;#requirements&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Requirements&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Before configuring SSO, ensure the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Add an OIDC Provider to your Cognito User Pool&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Requires: Client ID, Client Secret, Issuer ID&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Configure the &lt;code&gt;groups&lt;/code&gt; claim in your OIDC provider&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; This is required for group validation to function properly&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Add a custom &lt;code&gt;groups&lt;/code&gt; attribute in Cognito&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; The attribute name must match the &lt;code&gt;groups&lt;/code&gt; claim used by your OIDC providers&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&quot;provider-setup-guides&quot;&gt;Provider setup guides&lt;a href=&quot;#provider-setup-guides&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Provider setup guides&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;/blog/azure-sso-setup/&quot;&gt;Azure Single Sign-On (SSO) Setup: A Step-by-Step Guide&lt;/a&gt;
 
&lt;a href=&quot;/blog/okta-sso-setup/&quot;&gt;Okta Single Sign-On (SSO) Setup: A Step-by-Step Guide&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Other OIDC providers typically follow a similar setup process.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;creating-sso-account-configuration&quot;&gt;Creating SSO account configuration&lt;a href=&quot;#creating-sso-account-configuration&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Creating SSO account configuration&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;After entering the admin panel, find SSO Account Configs under the OIDC Auth application and click Add.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Django admin index with SSO Account Configs under the OIDC Auth application&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-1.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;setting-up-sso-account-configuration&quot;&gt;Setting up SSO account configuration&lt;a href=&quot;#setting-up-sso-account-configuration&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Setting up SSO account configuration&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To configure an SSO Client provider, you need the Client ID, Client Secret from you Cognito App Client, you can copy this in your App Client page.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Cognito App Client information page with the Client ID and Client secret copy buttons highlighted&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-2.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Choose your Account.&lt;br&gt;
&lt;strong&gt;Important:&lt;/strong&gt; Only accounts with the required permissions can be selected in this field by SSO requirements.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Enter your application Cognito Client ID, Cognito Client Secret.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Enter Provider Names from your Cognito App client, one of the values in this list can be used as &lt;code&gt;identity_provider&lt;/code&gt; in &lt;code&gt;/oauth2/authorize&lt;/code&gt; Cognito URI.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Enable Is active to activate this provider.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Allowed Domains — users whose email domain matches one of these domains will be allowed to access your application via this SSO configuration.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Enter the OIDC Provider Groups that should have access your application via this SSO configuration.&lt;br&gt;
&lt;strong&gt;Important:&lt;/strong&gt; Only users who belong to these groups will be able to sign in.&lt;br&gt;
&lt;strong&gt;Warning:&lt;/strong&gt; The group name must exactly match the format in your OIDC provider’s ID token.&lt;br&gt;
Make sure to check the correct group format (e.g., Okta = group name, Azure = group object ID).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Finally, click Save.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here is an example of a completed form for my Account and Cognito application client.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Completed SSO account configuration form in the Django admin&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-3.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;sso-notification-configuration&quot;&gt;SSO notification configuration&lt;a href=&quot;#sso-notification-configuration&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: SSO notification configuration&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;h3 id=&quot;azure&quot;&gt;Azure&lt;a href=&quot;#azure&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Azure&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;To configure SSO Provider Notification Config for Azure, you need the Client ID, Client Secret, and Tenant ID  from your Azure application (copied during the final step of the Azure configuration documentation).&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Choose Azure as a Provider Name.&lt;/li&gt;
&lt;li&gt;Choose your Azure SSO Provider.&lt;/li&gt;
&lt;li&gt;Enter your Azure application Client ID, Client Secret and Tenant ID.&lt;/li&gt;
&lt;li&gt;Enter Notification url to your application Notification URL Endpoint (in my example it is &lt;a href=&quot;https://dev.yourapp.com/api/oidc/notifications/azure&quot;&gt;https://dev.yourapp.com/api/oidc/notifications/azure&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Finally, click Save.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;okta&quot;&gt;Okta&lt;a href=&quot;#okta&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Okta&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;No additional configuration is required if Event Hooks are set up correctly.&lt;br&gt;
Soft-delete functionality will work automatically.&lt;/p&gt;
&lt;p&gt;✅ You have now successfully created SSO Providers in your application and can test it!&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;admin-api-automating-setup&quot;&gt;Admin API (automating setup)&lt;a href=&quot;#admin-api-automating-setup&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Admin API (automating setup)&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;h3 id=&quot;post-apioidccognitosetup&quot;&gt;POST /api/oidc/cognito/setup/&lt;a href=&quot;#post-apioidccognitosetup&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: POST /api/oidc/cognito/setup/&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Purpose:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;These endpoints automate the setup of SSO in Cognito and your app, reducing manual configuration and ensuring consistent integration between external OIDC providers and Cognito App Clients.&lt;/p&gt;
&lt;p&gt;Creates one or more External Identity Providers inside the Cognito User Pool and then creates a Cognito App Client that is linked to all providers specified in the providers field.&lt;/p&gt;
&lt;p&gt;!Important: This endpoint requires admin or privileged user access.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Request fields:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;provider&lt;/code&gt;&lt;/strong&gt; (required)&lt;/p&gt;
&lt;p&gt;An external OIDC provider to create in the Cognito User Pool.&lt;/p&gt;
&lt;p&gt;Each provider entry must include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;client_id - Identity Provider Client ID&lt;/li&gt;
&lt;li&gt;client_secret - Identity Provider Client Secret&lt;/li&gt;
&lt;li&gt;oidc_issuer - Identity Provider Issuer URL&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;group&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A group name used for group validation during the SSO login flow if the user does not exist in your app.&lt;/p&gt;
&lt;p&gt;Make sure to check the correct group format in your provider first (for example, for Okta it’s the group name, while for Azure it’s the group object ID). Otherwise, group validation won’t be working.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;allowed_domain&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Users with this email domain will be allowed to access your application via this SSO configuration.&lt;/p&gt;
&lt;p&gt;Example request for Okta:&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;json&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;{&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    &quot;provider&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        &quot;client_id&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;0oavpnkcas2Otd5s697&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt; // Identity Provider Client ID&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        &quot;client_secret&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;YRlFfB8wXkEkIasdWlmxC_UQenDNWTbCLmhasdGGBoDKsF00njihZ&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt; // Identity Provider Client Secret&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        &quot;oidc_issuer&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;https://integrator-2000932.okta.com/oauth2/default&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;  // Identity Provider Issuer URL (Okta Format)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    }&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    &quot;group&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;ada2a854-1asdd-475b-8e00-c7841dc99147&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt; // Okta format group&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    &quot;allowed_domain&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;test-company.com&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;  // Allowed domain for SSO Provider&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;Example Request for Azure AD:&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;json&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;{&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    &quot;provider&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        &quot;client_id&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;b4asdaf-315c-4236-964b-d0d6ebbe6725&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt; // Azure App Client ID&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        &quot;client_secret&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;XOQ8Q~iUoWahILy6mpzIvsoSsGZT7cii&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt; // Azure App Client Secret&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        &quot;oidc_issuer&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;https://login.microsoftonline.com/d277adb7-1729-42x2-98a0-asda0b7/v2.0&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;  // Azure App Issuer URL&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    }&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    &quot;group&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;asds2-1f3d-475b-8e00-c7841dc99147&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt; // group fom Azure AD&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    &quot;allowed_domain&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;test-company.com&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;  // Allowed domain for SSO Provider&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;Possible Errors:&lt;/p&gt;
&lt;p&gt;Provider does not exists in User Pool 400:&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;json&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;{&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    &quot;status&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;failed&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    &quot;message&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;Failed to create Cognito client or SSO provider.&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    &quot;error&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;{&apos;message&apos;: ErrorDetail(string=&apos;The provider TestProvider does not exist for User Pool eu-north-1_....&apos;, code=&apos;cognito_error&apos;)}&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;Provider with same name already exists in User Pool 400:&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;json&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;{&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    &quot;status&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;failure&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    &quot;message&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;Failed to create OIDC provider.&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    &quot;error&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;{&apos;message&apos;: ErrorDetail(string=&apos;OktaTestProvider already exists for tenant eu-north-1_....&apos;, code=&apos;cognito_error&apos;)}&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;Successful Response 201:&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;json&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;{&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    &quot;status&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;success&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    &quot;message&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;OIDC provider created successfully.&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;hr&gt;
&lt;h2 id=&quot;triggering-azure-notification-events&quot;&gt;Triggering Azure notification events&lt;a href=&quot;#triggering-azure-notification-events&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Triggering Azure notification events&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To trigger a soft-delete of a user in your application, you need to either remove the user from an Azure group or fully delete the user from Azure IAM.&lt;/p&gt;
&lt;h3 id=&quot;delete-user-from-group&quot;&gt;Delete user from group&lt;a href=&quot;#delete-user-from-group&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Delete user from group&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;To soft-delete a user from all rights holders where the user was, you need to remove the user from the corresponding Azure AD group (the group with the objectId defined in your SSO Client Provider).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Navigate to group&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Go to:&lt;br&gt;
Your Azure Active Directory instance → Groups → All groups&lt;/li&gt;
&lt;li&gt;Click on the Group to which you want to delete users.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Azure Active Directory All groups list with the target group selected&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-4.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Navigate to Members tab&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Click the “Members” button on the sidebar.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Azure group page with the Members item in the sidebar&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-5.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Remove members from the groups&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Select the users you want to remove from the group.&lt;/li&gt;
&lt;li&gt;After you select all users you want to delete from the group. Click Remove.&lt;/li&gt;
&lt;li&gt;A modal window will appear with the configuration form. Click Yes&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Azure group members list with users selected and the Remove button&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-6.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Azure confirmation dialog for removing members from the group&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-7.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;After this, if a member with this ObjectID exists in your app, the system will soft-delete the user.&lt;/p&gt;
&lt;h3 id=&quot;delete-user-from-iam&quot;&gt;Delete user from IAM&lt;a href=&quot;#delete-user-from-iam&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Delete user from IAM&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Navigate to Users page&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Go to:&lt;br&gt;
Your Azure Active Directory instance → Users.&lt;/li&gt;
&lt;li&gt;Select the users you want to delete.&lt;/li&gt;
&lt;li&gt;Click the “Delete” button, then confirm by clicking “Ok” in the modal window.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Azure Users list with users selected and the Delete button&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-8.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Azure confirmation dialog for deleting the selected users&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-9.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Navigate to Deleted users tab&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Click the “Deleted users” button on the sidebar.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Azure Users page with the Deleted users item in the sidebar&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-10.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Permanently delete the user&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Select the user you delete in Step 1 or you want to delete.&lt;/li&gt;
&lt;li&gt;Click “Delete permanently”, then confirm by clicking “Ok” in the modal window.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Azure Deleted users list with a user selected and the Delete permanently button&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-11.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Azure confirmation dialog for permanently deleting the user&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-12.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;After this, the selected users will be completely deleted from Azure IAM.&lt;br&gt;
If a user with the same User ObjectID exists in your app, that user will also be soft-deleted from your app.&lt;/p&gt;
&lt;p&gt;Note: The User ObjectID is linked to a user in your app only after the user logs into your app via the Azure SSO Provider.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;okta-triggering-notification-events&quot;&gt;Okta: triggering notification events&lt;a href=&quot;#okta-triggering-notification-events&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Okta: triggering notification events&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To trigger a soft-delete of a user in your application, you need to either remove the user from an Okta group or fully delete the user from Okta IAM.&lt;/p&gt;
&lt;h3 id=&quot;delete-user-from-group-1&quot;&gt;Delete user from group&lt;a href=&quot;#delete-user-from-group-1&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Delete user from group&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;To soft-delete a user from all rights holders where the user was, you need to remove the user from the corresponding Okta group (the group with the Group name defined in your SSO Client Provider).&lt;/p&gt;
&lt;p&gt;In the side menu, navigate to the Groups page and click on the group which you want to remove users:&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Okta Groups page in the admin console with the target group&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-13.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;Next to the user you want to remove, click on the cross (x) icon and the user will be removed.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Okta group members list with the remove (x) icon next to a user&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-14.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;Once the user is removed from the group, they will be soft-deleted in your application.&lt;/p&gt;
&lt;p&gt;Note: Users will be removed only when deleted from groups that are present in your app SSO Providers Group name, if not this event hook will be skipped.&lt;/p&gt;
&lt;h3 id=&quot;delete-user-from-iam-1&quot;&gt;Delete user from IAM&lt;a href=&quot;#delete-user-from-iam-1&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Delete user from IAM&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In the side menu, navigate to the People page and select the the user which you want to delete:&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Okta People page with the user to delete&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-15.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;You will be redirected to the User Profile page. Click on the More Actions button, then click Deactivate to deactivate the user.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Okta user profile with the More Actions menu open and Deactivate option&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-16.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;A modal window will appear with. Click Deactivate.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Okta confirmation dialog for deactivating the user&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-17.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;Then after the user deactivated, the Delete button will appear. Click Delete to completely delete the user from IAM.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Okta deactivated user profile with the Delete button&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-18.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;A modal window will appear with. Click Delete.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Okta confirmation dialog for deleting the user&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-19.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;After this, the selected user will be completely deleted from Okta IAM.&lt;/p&gt;
&lt;p&gt;If a user with the same email exists in your application, that user will also be soft-deleted in your application.&lt;/p&gt;
</content:encoded><category>sso</category><category>authentication</category><category>django</category><category>cognito</category></item><item><title>How to test SSO authentication with Cognito and OIDC providers</title><link>https://serhiisoldatov.dev/blog/testing-sso-with-cognito-and-oidc/</link><guid isPermaLink="true">https://serhiisoldatov.dev/blog/testing-sso-with-cognito-and-oidc/</guid><description>How to test SSO login and logout through AWS Cognito with an external OIDC provider, check that the user was created, and what a failed group validation looks like.</description><pubDate>Sun, 12 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;prerequisites-for-all-oidc-providers&quot;&gt;Prerequisites for all OIDC providers&lt;a href=&quot;#prerequisites-for-all-oidc-providers&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Prerequisites for all OIDC providers&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This guide covers the admin setup for SSO authentication built with Django and AWS Cognito. The full reference implementation is available on GitHub: &lt;a href=&quot;https://github.com/soldatov-ss/django-cognito-sso-demo&quot;&gt;django-cognito-sso-demo&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;To ensure SSO works correctly in your application, you must complete the following steps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Add OIDC Provider to the Cognito user pool to External Providers, for this you need &lt;strong&gt;Client ID, Client Secret&lt;/strong&gt; and &lt;strong&gt;Issuer ID&lt;/strong&gt; of OIDC Provider.&lt;/li&gt;
&lt;li&gt;Make sure that &lt;strong&gt;SSO Provider&lt;/strong&gt; with this App Client credentials exists on your application.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Configure the “groups” claim&lt;/strong&gt; in the ID Token on your OIDC provider.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is required for group validation to function properly in SSO.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Add a custom &lt;strong&gt;group&lt;/strong&gt; claim to Cognito by creating a custom attribute named &lt;strong&gt;groups&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The name must match the “groups” claim used in all OIDC providers that will be included in your application SSO.&lt;/p&gt;
&lt;p&gt;After setting up the Admin Panel, we are ready to test SSO authentication.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;provider-setup-guides&quot;&gt;Provider setup guides&lt;a href=&quot;#provider-setup-guides&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Provider setup guides&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;/blog/azure-sso-setup/&quot;&gt;Azure Single Sign-On (SSO) Setup: A Step-by-Step Guide&lt;/a&gt;
 
&lt;a href=&quot;/blog/okta-sso-setup/&quot;&gt;Okta Single Sign-On (SSO) Setup: A Step-by-Step Guide&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Other OIDC providers typically follow a similar setup process.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;log-in-via-cognito&quot;&gt;Log in via Cognito&lt;a href=&quot;#log-in-via-cognito&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Log in via Cognito&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; To log in to your application via the Azure SSO Provider, send a GET request to:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;cognito_domain&amp;gt;/login?client_id=&amp;lt;client_id&amp;gt;&amp;amp;redirect_uri=&amp;lt;redirect_uri&amp;gt;&amp;amp;response_type=code&amp;amp;scope=openid&amp;amp;state=&amp;lt;client_id&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;where:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;cognito_domain -&lt;/strong&gt; the AWS Cognito Domain&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;region&lt;/strong&gt; - the AWS region where your Cognito User Pool is hosted (e.g., eu-central-1);&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;client_id&lt;/strong&gt; - the Cognito App Client ID for the specific tenant (client);&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;redirect_uri&lt;/strong&gt; - the callback URL configured in Cognito for your application (e.g., &lt;a href=&quot;https://yourapp.com/api/oidc/cognito/callback/&quot;&gt;https://yourapp.com/api/oidc/cognito/callback/&lt;/a&gt;);&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;state&lt;/strong&gt; - a parameter used to maintain state between the request and callback (here you must reuse the &lt;strong&gt;client_id&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;Click on your Continue with your OIDC Provider name, that you enter in Cognito.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Cognito managed login page with a Continue with button for the OIDC provider&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-1.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2&lt;/strong&gt; After sending the request, you will be redirected to the Your OIDC Provider login page, in my case it’s Microsoft login page. Click &lt;strong&gt;Receive Code.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Microsoft login page with the Receive Code option&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-2.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3&lt;/strong&gt;:  Enter the code sent to &lt;strong&gt;user_email&lt;/strong&gt;, then click &lt;strong&gt;Yes&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Microsoft prompt to enter the code sent to the user email&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-3.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 4:&lt;/strong&gt; After successful authentication, you will receive a response with your authentication tokens.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Callback response containing the authentication tokens after a successful SSO login&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-4.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 5:&lt;/strong&gt; To check if a user was actually created after SSO login, you can:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Check via the Admin Panel:&lt;/strong&gt; Go to &lt;strong&gt;Admin Panel → Users&lt;/strong&gt; and search for the user by email.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Check via API:&lt;/strong&gt; Send a GET request to: &lt;code&gt;/api/users/me/&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;testing-log-out&quot;&gt;Testing log out&lt;a href=&quot;#testing-log-out&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Testing log out&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To log out, send a POST or GET request to:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/api/oidc/cognito/logout/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;After successful logout you will receive a response with your message “Logout successful”.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Logout endpoint response with the message Logout successful&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-5.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;group-validation&quot;&gt;Group validation&lt;a href=&quot;#group-validation&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Group validation&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If the user is &lt;strong&gt;not a member&lt;/strong&gt; of the IAM group, the &lt;strong&gt;group name&lt;/strong&gt; is incorrect in the Admin Panel for the SSO Provider, or the user’s &lt;strong&gt;email domain&lt;/strong&gt; is not linked with a rights holder of the &lt;strong&gt;app_id&lt;/strong&gt;, the login attempt will fail, showing an error, where &lt;strong&gt;provider 19&lt;/strong&gt; is &lt;strong&gt;SSO Client Provider ID&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Error response for a failed SSO login due to group validation for provider 19&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-6.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;✅ &lt;strong&gt;You have now successfully SSO authentication!&lt;/strong&gt;&lt;/p&gt;
</content:encoded><category>sso</category><category>testing</category><category>cognito</category><category>oidc</category></item><item><title>How to configure AWS Cognito for SSO: a step-by-step guide</title><link>https://serhiisoldatov.dev/blog/aws-cognito-sso-setup/</link><guid isPermaLink="true">https://serhiisoldatov.dev/blog/aws-cognito-sso-setup/</guid><description>How to set up AWS Cognito for multi-tenant SSO: an app client per customer, a custom groups attribute, external OIDC providers, and a managed login page.</description><pubDate>Sun, 12 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;creating-app-client-for-each-customer&quot;&gt;Creating app client for each customer&lt;a href=&quot;#creating-app-client-for-each-customer&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Creating app client for each customer&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;According to the multi-tenant application model, we need to create a separate Cognito App Client for each customer and attach their corresponding external OIDC providers.&lt;/p&gt;
&lt;h3 id=&quot;step-1-creating-application-client&quot;&gt;Step 1: Creating application client&lt;a href=&quot;#step-1-creating-application-client&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Step 1: Creating application client&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;To create App Client navigate to &lt;strong&gt;Application - App client,&lt;/strong&gt; click to &lt;strong&gt;Create App Client&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Cognito App clients page with the Create app client button&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-1.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;In the setup form, choose &lt;strong&gt;Traditional web application&lt;/strong&gt; as the application type.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Cognito app client setup form with Traditional web application selected&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-2.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;step-2-adding-custom-attribute&quot;&gt;Step 2: Adding custom attribute&lt;a href=&quot;#step-2-adding-custom-attribute&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Step 2: Adding custom attribute&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;To include user groups in the ID token, we need to add a custom attribute to the Cognito User Pool.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Authentication&lt;/strong&gt; &lt;strong&gt;-&lt;/strong&gt; &lt;strong&gt;Sign Up&lt;/strong&gt;, then click &lt;strong&gt;Add custom attribute:&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Cognito Sign-up page with the Add custom attribute button&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-3.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;In the form, enter the attribute name as shown below, and then click &lt;strong&gt;Save changes&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Cognito custom attribute form with the attribute name groups&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-4.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;adding-roles-custom-attribute&quot;&gt;Adding roles custom attribute&lt;a href=&quot;#adding-roles-custom-attribute&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Adding roles custom attribute&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;If “&lt;strong&gt;roles&lt;/strong&gt;” scope is configured in Customer OIDC provider, you can add a custom “&lt;strong&gt;roles&lt;/strong&gt;” attribute as well and include this attribute in External Cognito Provider.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Cognito custom attribute form with the attribute name roles&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-5.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;step-3-creating-external-provider&quot;&gt;Step 3: Creating external provider&lt;a href=&quot;#step-3-creating-external-provider&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Step 3: Creating external provider&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Authentication - Social and external providers&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Cognito Social and external providers page&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-6.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;In from enter your OIDC Provider credentials, &lt;strong&gt;Client ID&lt;/strong&gt;, &lt;strong&gt;Client Secret&lt;/strong&gt; and &lt;strong&gt;Issuer URI&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Add custom “&lt;strong&gt;groups”&lt;/strong&gt; attributes that you created in the previous step and include &lt;strong&gt;email&lt;/strong&gt; attributes. (username will be added automatically after you created the provider).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Cognito Edit identity provider form for Azure with client ID, client secret and authorized scopes&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-7.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Cognito identity provider form with the Auto fill through issuer URL setup method and the Azure issuer URL&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-8.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;!Important:&lt;/strong&gt; In the form, enter the OpenID Connect attribute to &lt;strong&gt;groups&lt;/strong&gt; exactly as shown below, and then click &lt;strong&gt;Save changes&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Clarification:&lt;/strong&gt; We configure these attributes so that the group and role information (optional) is included in the &lt;strong&gt;ID Token&lt;/strong&gt; returned by the Identity Provider. If your application uses different scope names, make sure to update the values in the Cognito accordingly (for example, if Provider sends roles under &lt;strong&gt;app_groups&lt;/strong&gt; scope, you should set OpenID Connect attribute to &lt;strong&gt;app_groups&lt;/strong&gt;).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Info:&lt;/strong&gt; If you added a “&lt;strong&gt;custom:roles&lt;/strong&gt;” attribute in the previous step, you can include the “&lt;strong&gt;roles&lt;/strong&gt;” attribute here as well.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Cognito attribute mapping with custom:groups mapped to the groups OpenID Connect attribute&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-9.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;step-4-attach-external-provider-to-app-client&quot;&gt;Step 4: Attach external provider to app client&lt;a href=&quot;#step-4-attach-external-provider-to-app-client&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Step 4: Attach external provider to app client&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;App Clients - Your App - Login Pages&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Edit&lt;/strong&gt; button.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Cognito app client Login pages tab with the Edit button&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-10.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;In the configuration form:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Add allowed callback URLs (e.g.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://yourapp.com/api/oidc/cognito/callback/&quot;&gt;https://yourapp.com/api/oidc/cognito/callback/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;or &lt;strong&gt;&lt;a href=&quot;http://localhost:8000/api/oidc/cognito/callback/&quot;&gt;http://localhost:8000/api/oidc/cognito/callback/&lt;/a&gt;&lt;/strong&gt; for local testing).&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;Attach the identity providers you created in the previous steps.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;!Important&lt;/strong&gt;: Under &lt;strong&gt;OpenID Connect scopes&lt;/strong&gt;, select only the &lt;strong&gt;OpenId&lt;/strong&gt; scope.&lt;/li&gt;
&lt;li&gt;Click Save changes.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Cognito managed login pages configuration with callback URLs, identity providers and the openid scope&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-11.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;After &lt;strong&gt;custom attributes&lt;/strong&gt; setup check in &lt;strong&gt;Attribute Permission&lt;/strong&gt; that &lt;strong&gt;Read&lt;/strong&gt; and &lt;strong&gt;Write&lt;/strong&gt; permissions are checked:&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Cognito attribute read and write permissions with the custom attributes checked&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-12.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;step-5-attach-login-page-to-the-app-client&quot;&gt;Step 5: Attach login page to the app client&lt;a href=&quot;#step-5-attach-login-page-to-the-app-client&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Step 5: Attach login page to the app client&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Your User Pool - Managed login.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create a style&lt;/strong&gt; button.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Cognito Managed login page with the Create a style button&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-13.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;Select the &lt;strong&gt;App client&lt;/strong&gt; you want to attach a &lt;strong&gt;login page&lt;/strong&gt; to.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Cognito Create style dialog with the app client selected&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-14.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;After creation, the login page for your App Client will become available and can be accessed directly via its generated URL.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Cognito managed login style created for the app client with its login page URL&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-15.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;how-to-connect-to-your-application&quot;&gt;How to connect to your application&lt;a href=&quot;#how-to-connect-to-your-application&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: How to connect to your application&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To enable &lt;strong&gt;Cognito login&lt;/strong&gt; for your users in &lt;strong&gt;your application&lt;/strong&gt;, you need to retrieve the following three parameters:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Client ID&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Client Secret&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User Pool Cognito domain&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You can find this data on our Cognito User Pool and App client pages.&lt;/p&gt;
&lt;p&gt;To copy &lt;strong&gt;Client ID&lt;/strong&gt; and &lt;strong&gt;Client Secret&lt;/strong&gt; Navigate to corresponding &lt;strong&gt;App client:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Cognito app client page showing the Client ID and Client secret&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-16.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;To get User Pool &lt;strong&gt;Cognito domain&lt;/strong&gt;, navigate to &lt;strong&gt;Domain&lt;/strong&gt; page:&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Cognito user pool Domain page showing the Cognito domain&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-17.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;✅ &lt;strong&gt;Cognito Setup Complete.&lt;/strong&gt; &lt;strong&gt;Your Cognito configuration is now complete and ready for SSO testing.&lt;/strong&gt;&lt;/p&gt;
</content:encoded><category>aws</category><category>cognito</category><category>sso</category><category>oidc</category></item><item><title>Okta single sign-on (SSO) setup: a step-by-step guide</title><link>https://serhiisoldatov.dev/blog/okta-sso-setup/</link><guid isPermaLink="true">https://serhiisoldatov.dev/blog/okta-sso-setup/</guid><description>How to set up an Okta OIDC application for SSO: groups, role attributes, scopes, claims, access policies, event hooks for soft-deleting users, and optional MFA.</description><pubDate>Sat, 11 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;configuring-sso-authentication-in-okta&quot;&gt;Configuring SSO authentication in Okta&lt;a href=&quot;#configuring-sso-authentication-in-okta&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Configuring SSO authentication in Okta&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;After logging into the Okta panel, you will be redirected to the dashboard. From here, we will configure Okta to enable Single Sign-On (SSO) authentication for our application.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Okta admin dashboard after signing in&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-1.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;assigning-users-to-group&quot;&gt;Assigning users to group&lt;a href=&quot;#assigning-users-to-group&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Assigning users to group&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;We need to add groups and add the required users to the appropriate group.&lt;/p&gt;
&lt;p&gt;!Note: Only users who are &lt;strong&gt;members&lt;/strong&gt; of this &lt;strong&gt;group&lt;/strong&gt; will be able to sign in to your application.&lt;/p&gt;
&lt;p&gt;In the side menu, navigate to the &lt;strong&gt;Groups&lt;/strong&gt; page&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Okta side menu with the Groups page selected&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-2.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;then click to the &lt;strong&gt;Add group&lt;/strong&gt; button&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Groups page with the Add group button&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-3.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;A modal window will appear with the configuration form.&lt;/p&gt;
&lt;p&gt;Fill this form with the group name and description, then click the &lt;strong&gt;Save&lt;/strong&gt; button.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Add group modal with the group name and description filled in&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-4.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;In the side menu, navigate to the &lt;strong&gt;People&lt;/strong&gt; page, then choose a particular person in the list and click to navigate to the &lt;strong&gt;Person&lt;/strong&gt; page&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;People page listing users in Okta&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-5.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;Click on the &lt;strong&gt;Groups&lt;/strong&gt; subtitle to navigate to the profile page&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Person page with the Groups tab&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-6.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;Click on the &lt;strong&gt;Search&lt;/strong&gt; field and enter the group name that you want to assign the user with, then click to the appearing group.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Searching for a group name to assign the user to&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-7.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;creating-an-application&quot;&gt;Creating an application&lt;a href=&quot;#creating-an-application&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Creating an application&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;An application refers to a cloud or on-premises service integrated with Okta for single sign-on, enabling users to securely access and authenticate across multiple applications with a single set of credentials.&lt;/p&gt;
&lt;p&gt;Next, navigate to the &lt;strong&gt;Applications&lt;/strong&gt; page (as shown in the screenshot)&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Okta side menu with the Applications page selected&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-8.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;Click on the &lt;strong&gt;Create App Integration&lt;/strong&gt; button&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Applications page with the Create App Integration button&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-9.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;A modal window will appear with configuration options. Select the options as shown in the screenshot, then click the &lt;strong&gt;Next&lt;/strong&gt; button.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Create a new app integration modal with the sign-in method and application type selected&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-10.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;You will be redirected to the configuration page:&lt;/p&gt;
&lt;p&gt;Follow the instructions below and configure the settings as marked in the next two screenshots:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;I will set my app name to “SSO OIDC”, but you can use just “your application”.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sign-in redirect URIs:&lt;/strong&gt; The format should be:
&lt;code&gt;https://&amp;lt;your-user-pool-domain&amp;gt;/oauth2/idpresponse&lt;/code&gt;
where &lt;code&gt;&amp;lt;your-user-pool-domain&amp;gt;&lt;/code&gt; is your Cognito User Pool Domain&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;(example: &lt;strong&gt;&lt;a href=&quot;https://eu-north-2uasd2wr7mv.auth.eu-north-1.amazoncognito.com/oauth2/idpresponse&quot;&gt;https://eu-north-2uasd2wr7mv.auth.eu-north-1.amazoncognito.com/oauth2/idpresponse&lt;/a&gt;&lt;/strong&gt;).&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;&lt;strong&gt;Sign-out redirect URIs:&lt;/strong&gt; This ensures users are properly signed out when logging out of our application.
Input: &lt;code&gt;https://yourapp.com/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Right holder access:&lt;/strong&gt; choose the &lt;strong&gt;Limit access to selected groups&lt;/strong&gt; option and enter group names you want to give access to &lt;strong&gt;your application&lt;/strong&gt; application.&lt;/li&gt;
&lt;li&gt;Click on the &lt;strong&gt;Save&lt;/strong&gt; button.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;New app integration settings with the app name and sign-in redirect URIs&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-11.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Sign-out redirect URIs and assignment settings limited to selected groups&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-12.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;You will then be redirected to the newly created application’s page.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Newly created SSO OIDC application page&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-13.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;configuring-a-user-okta-profile-add-roles-to-profile&quot;&gt;Configuring a user Okta profile (add roles to profile)&lt;a href=&quot;#configuring-a-user-okta-profile-add-roles-to-profile&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Configuring a user Okta profile (add roles to profile)&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In &lt;strong&gt;Okta&lt;/strong&gt;, we can define roles for users to manage permissions within our application.&lt;/p&gt;
&lt;p&gt;These roles determine the level of access a user has, such as &lt;strong&gt;admin&lt;/strong&gt;, &lt;strong&gt;manager&lt;/strong&gt;, etc.&lt;/p&gt;
&lt;p&gt;In the side menu, navigate to the &lt;strong&gt;Profile Editor&lt;/strong&gt; page&lt;/p&gt;
&lt;p&gt;Click to &lt;strong&gt;User (default)&lt;/strong&gt; link to open User Okta Profile edit page:&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Profile Editor page with the User (default) profile link&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-14.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;Click on the &lt;strong&gt;Add Attribute&lt;/strong&gt; button to add a new attribute for roles in the Okta User profile:&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;User (default) profile with the Add Attribute button&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-15.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;A modal window will appear with configuration form.&lt;/p&gt;
&lt;p&gt;Fill this form as shown in the following screenshot, then click the &lt;strong&gt;Save&lt;/strong&gt; button.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Add Attribute modal filled in for the user roles attribute&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-16.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;configuring-an-oidc-client-profile-for-the-app&quot;&gt;Configuring an OIDC client profile for the app&lt;a href=&quot;#configuring-an-oidc-client-profile-for-the-app&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Configuring an OIDC client profile for the app&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In the side menu, navigate to the &lt;strong&gt;Profile Editor&lt;/strong&gt; page:&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Okta side menu with the Profile Editor page selected&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-17.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;Click on the &lt;strong&gt;SSO OIDC User&lt;/strong&gt; (&lt;strong&gt;name of our application&lt;/strong&gt;) link to navigate to the OIDC Client Profile edit page:&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Profile Editor listing the SSO OIDC User app profile&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-18.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;Click on the &lt;strong&gt;Add Attribute&lt;/strong&gt; button to add a new attribute for roles in the OIDC Client Profile.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;SSO OIDC User profile with the Add Attribute button&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-19.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;A modal window will appear with configuration form.&lt;/p&gt;
&lt;p&gt;Fill this form as shown in the following screenshot, then click the &lt;strong&gt;Save&lt;/strong&gt; button.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Add Attribute modal filled in for the roles attribute on the OIDC client profile&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-20.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;Click on the &lt;strong&gt;Mappings&lt;/strong&gt; button to configure the mapping between the Okta User Profile and the OIDC Client Profile:&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;SSO OIDC User profile with the Mappings button&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-21.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;A modal window will appear with configuration fields for each relation.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Mappings modal showing a field for each profile attribute&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-22.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;Slide to the last field, type in the field &lt;code&gt;user.userRoles&lt;/code&gt;. Click on the relation button which mark on the following screenshot:&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Mapping user.userRoles to the roles attribute, with the relation button highlighted&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-23.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;Choose an &lt;strong&gt;Apply mapping on user create and update&lt;/strong&gt; option:&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Mapping relation menu with Apply mapping on user create and update selected&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-24.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;Now, this should look like as shown below. Click on the &lt;strong&gt;Save Mappings&lt;/strong&gt; button.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Completed attribute mapping before clicking Save Mappings&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-25.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;configuring-an-api-authorization&quot;&gt;Configuring an API authorization&lt;a href=&quot;#configuring-an-api-authorization&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Configuring an API authorization&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In the side menu, navigate to the &lt;strong&gt;API&lt;/strong&gt; page&lt;/p&gt;
&lt;p&gt;Click on the &lt;strong&gt;default&lt;/strong&gt; link (I use &lt;strong&gt;default_2&lt;/strong&gt;, because &lt;strong&gt;default&lt;/strong&gt; in my account already configured), which is marked in the following screenshot:&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;API page listing authorization servers with default_2 highlighted&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-26.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;configuring-a-group-scope&quot;&gt;Configuring a group scope&lt;a href=&quot;#configuring-a-group-scope&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Configuring a group scope&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Now, let’s configure scopes to define the permissions our application can request.&lt;/p&gt;
&lt;p&gt;Click on the &lt;strong&gt;Scopes&lt;/strong&gt; subtitle to navigate to the scopes configuration page:&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Authorization server page with the Scopes tab&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-27.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;Click on the &lt;strong&gt;Add Scope&lt;/strong&gt; button to add a new scope, which will be used for groups&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Scopes tab with the Add Scope button&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-28.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;A modal window will appear with configuration form. Fill this form as shown in the following screenshot, then click the &lt;strong&gt;Create&lt;/strong&gt; button.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;!Important&lt;/strong&gt;: &lt;strong&gt;Scope name&lt;/strong&gt; must be &lt;strong&gt;“groups”&lt;/strong&gt;, values in the &lt;strong&gt;“Display phrase”&lt;/strong&gt; and &lt;strong&gt;“Description”&lt;/strong&gt; fields are not that important.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Add Scope modal with the scope name set to groups&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-29.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;configuring-role-scope&quot;&gt;Configuring role scope&lt;a href=&quot;#configuring-role-scope&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Configuring role scope&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Click on the &lt;strong&gt;Add Scope&lt;/strong&gt; button to add a new scope, which will be used for roles&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Scopes tab with the Add Scope button for the roles scope&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-30.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;A modal window will appear with configuration form. Fill this form as shown in the following screenshot, then click the &lt;strong&gt;Create&lt;/strong&gt; button.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Add Scope modal filled in for the roles scope&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-31.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;configuring-a-new-groups-claim&quot;&gt;Configuring a new groups claim&lt;a href=&quot;#configuring-a-new-groups-claim&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Configuring a new groups claim&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Configuring a claim after the scope for Okta SSO ensures that the appropriate user attributes are securely included in the authentication token, enabling seamless access control and personalized experiences across integrated applications.&lt;/p&gt;
&lt;p&gt;Click on the &lt;strong&gt;Claims&lt;/strong&gt; subtitle to navigate to the claims configuration page:&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Authorization server page with the Claims tab&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-32.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;Click on the &lt;strong&gt;Add Claim&lt;/strong&gt; button to add a new claim, which will be used for groups:&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Claims tab with the Add Claim button&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-33.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;A modal window will appear with configuration form. Fill this form as shown in the following screenshot, then click the &lt;strong&gt;Create&lt;/strong&gt; button.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;!Important&lt;/strong&gt;: &lt;strong&gt;Claim name&lt;/strong&gt; must be &lt;strong&gt;“groups”&lt;/strong&gt; and &lt;strong&gt;all&lt;/strong&gt; other values must be identical as on screenshot!&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Add Claim modal configured for the groups claim&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-34.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;configuring-a-new-roles-claim&quot;&gt;Configuring a new roles claim&lt;a href=&quot;#configuring-a-new-roles-claim&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Configuring a new roles claim&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Click on the &lt;strong&gt;Add Claim&lt;/strong&gt; button to add a new claim, which will be used for &lt;strong&gt;roles&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;A modal window will appear with configuration form. Fill this form as shown in the following screenshot, then click the &lt;strong&gt;Create&lt;/strong&gt; button.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Add Claim modal configured for the roles claim&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-35.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;configuring-a-new-access-policy&quot;&gt;Configuring a new access policy&lt;a href=&quot;#configuring-a-new-access-policy&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Configuring a new access policy&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Access policies and rules in Okta SSO allow administrators to define and enforce conditions for user authentication, ensuring secure and granular control over who can access applications based on factors like location, device, and user group.&lt;/p&gt;
&lt;p&gt;Click on the &lt;strong&gt;Access Policies&lt;/strong&gt; subtitle to navigate to the access policies configuration page:&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Authorization server page with the Access Policies tab&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-36.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;Click on the &lt;strong&gt;Add Policy&lt;/strong&gt; button to add a new policy:&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Access Policies tab with the Add Policy button&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-37.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;A modal window will appear with a configuration form. Fill this form as shown in the following screenshot, then click the &lt;strong&gt;Create Policy&lt;/strong&gt; button.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Add Policy modal filled in for the application&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-38.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;Click on the &lt;strong&gt;Add rule&lt;/strong&gt; button to add a new rule&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Access policy with the Add rule button&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-39.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;A modal window will appear with the configuration form. Fill in this form as shown in the screenshot.&lt;/p&gt;
&lt;p&gt;In the &lt;strong&gt;“User is”&lt;/strong&gt; section, select the group you want to allow access to your application.
In my case, it is the users from &lt;strong&gt;“test_group”&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;You can also add individual users if needed.&lt;/p&gt;
&lt;p&gt;Then, click the &lt;strong&gt;Create rule&lt;/strong&gt; button.&lt;/p&gt;
&lt;p&gt;Only users who are members of the selected group (and assigned the app) will be able to sign in.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Add Rule modal with access limited to users in test_group&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-40.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;configuring-a-new-event-hooks&quot;&gt;Configuring a new event hooks&lt;a href=&quot;#configuring-a-new-event-hooks&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Configuring a new event hooks&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Event Hooks in &lt;strong&gt;Okta SSO&lt;/strong&gt; allow your application to track IAM related events.
We will create an event hook that listens for two specific events:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;User removed from group&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User deleted&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These events will trigger a &lt;strong&gt;soft delete&lt;/strong&gt; of the corresponding user in your application.&lt;/p&gt;
&lt;p&gt;In the side menu, navigate to the &lt;strong&gt;Event hooks&lt;/strong&gt; page:&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Okta side menu with the Event Hooks page selected&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-41.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;Click on the &lt;strong&gt;Create Event Hook&lt;/strong&gt; button to add an event hook&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Event Hooks page with the Create Event Hook button&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-42.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;A modal window will appear with a configuration form.&lt;/p&gt;
&lt;p&gt;Fill this form as shown in the following screenshot, then click the &lt;strong&gt;Save &amp;amp; Continue&lt;/strong&gt; button.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Create Event Hook form with the endpoint and the user removed from group and user deleted events&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-43.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;Another more modal window will appear with a configuration form.&lt;/p&gt;
&lt;p&gt;Click &lt;strong&gt;Verify&lt;/strong&gt; to confirm that the webhook is active and reachable.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Event hook verification modal with the Verify button&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-44.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;You will then be redirected to the newly created event hook page.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Newly created event hook page&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-45.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;assigning-user-roles&quot;&gt;Assigning user roles&lt;a href=&quot;#assigning-user-roles&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Assigning user roles&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In the side menu, navigate to the &lt;strong&gt;People&lt;/strong&gt; page, then choose a particular person in the list and click to navigate to the &lt;strong&gt;Person&lt;/strong&gt; page&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;People page with a user selected&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-46.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;Click on the &lt;strong&gt;Profile&lt;/strong&gt; subtitle to navigate to the profile page&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Person page with the Profile tab&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-47.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;Click on the &lt;strong&gt;Edit&lt;/strong&gt; button to enable editing attributes of the profile. Slide down to the bottom of the page.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;User profile with the Edit button&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-48.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;Now, you can choose any number of roles from the list for the particular user. Click the &lt;strong&gt;Save&lt;/strong&gt; button.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Selecting roles for the user in the profile before saving&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-49.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;deleting-users&quot;&gt;Deleting users&lt;a href=&quot;#deleting-users&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Deleting users&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;h3 id=&quot;delete-user-from-group&quot;&gt;Delete user from group&lt;a href=&quot;#delete-user-from-group&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Delete user from group&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;To trigger a &lt;strong&gt;soft-delete&lt;/strong&gt; of a user in your application, you need to either remove the user from an Okta group or fully delete the user from Okta IAM.&lt;/p&gt;
&lt;p&gt;To soft-delete a user from all rights holders where the user was, you need to remove the user from the corresponding Okta group (the group with the &lt;strong&gt;Group name&lt;/strong&gt; defined in your SSO Client Provider).&lt;/p&gt;
&lt;p&gt;In the side menu, navigate to the &lt;strong&gt;Groups&lt;/strong&gt; page and click on the group which you want to remove users:&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Groups page with the group to remove users from&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-50.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;Next to the user you want to remove, click on the cross (x) icon and the user will be removed.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Group members list with the remove (x) icon next to a user&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-51.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;Once the user is removed from the group, they will be &lt;strong&gt;soft-deleted&lt;/strong&gt; in your application.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Users will be removed only when deleted from groups that are present in your app SSO Providers Group name, if not this event hook will be skipped.&lt;/p&gt;
&lt;h3 id=&quot;delete-user-from-iam&quot;&gt;Delete user from IAM&lt;a href=&quot;#delete-user-from-iam&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Delete user from IAM&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In the side menu, navigate to the &lt;strong&gt;People&lt;/strong&gt; page and select the the user which you want to delete:&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;People page with the user to delete selected&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-52.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;You will be redirected to the User Profile page. Click on the &lt;strong&gt;More Actions&lt;/strong&gt; button, then click &lt;strong&gt;Deactivate&lt;/strong&gt; to deactivate the user.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;User profile More Actions menu with the Deactivate option&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-53.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;A modal window will appear with. Click &lt;strong&gt;Deactivate.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Deactivate user confirmation modal&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-54.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;Then after the user deactivated, &lt;strong&gt;the Delete&lt;/strong&gt; button will appear. Click &lt;strong&gt;Delete&lt;/strong&gt; to completely delete the user from IAM.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Deactivated user profile with the Delete button&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-55.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;A modal window will appear with. Click &lt;strong&gt;Delete.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Delete user confirmation modal&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-56.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;After this, the selected user will be completely deleted from &lt;strong&gt;Okta IAM&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;If a user with the &lt;strong&gt;same email&lt;/strong&gt; exists in your application, that user will also be &lt;strong&gt;soft-deleted&lt;/strong&gt; in your application.&lt;/p&gt;
&lt;h2 id=&quot;optional-multi-factor-authentication&quot;&gt;(Optional) Multi factor authentication&lt;a href=&quot;#optional-multi-factor-authentication&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: (Optional) Multi factor authentication&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;h3 id=&quot;configuring-global-session-policy&quot;&gt;Configuring global session policy&lt;a href=&quot;#configuring-global-session-policy&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Configuring global session policy&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Multi-Factor Authentication (MFA) adds an additional layer of security to user login. When enabled, users must verify their identity using a second factor, such as the &lt;strong&gt;Okta Verify app&lt;/strong&gt;, in addition to their password.&lt;/p&gt;
&lt;p&gt;In the side menu, navigate to the &lt;strong&gt;Global Session Policy&lt;/strong&gt; page. Сlick &lt;strong&gt;Add policy&lt;/strong&gt; button&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Global Session Policy page with the Add policy button&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-57.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;A modal window will appear. Enter &lt;strong&gt;Policy name, Policy Description&lt;/strong&gt; and assign it to the group you want to enforce MFA for. In my case this will be a group of &lt;strong&gt;Everyone&lt;/strong&gt;. Click &lt;strong&gt;Create policy and add rule&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Add policy modal with the name, description and Everyone group assigned&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-58.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;Another modal window will appear with adding policy rule. Enter &lt;strong&gt;Rule name,&lt;/strong&gt; Set &lt;strong&gt;MFA&lt;/strong&gt; to &lt;strong&gt;Required,&lt;/strong&gt; In &lt;strong&gt;User will be prompted for MFA,&lt;/strong&gt; select the desired option. Then click &lt;strong&gt;Create rule.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Add rule modal with MFA set to Required&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-59.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Remaining global session policy rule settings with the Create rule button&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-60.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;configuring-authentication-policies&quot;&gt;Configuring authentication policies&lt;a href=&quot;#configuring-authentication-policies&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Configuring authentication policies&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In the side menu, navigate to the &lt;strong&gt;Authentication Policies&lt;/strong&gt; page and click on &lt;strong&gt;App sign-in&lt;/strong&gt; button:&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Authentication Policies page with the App sign-in tab&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-61.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;You will be redirected to the &lt;strong&gt;App sign-in policies&lt;/strong&gt; page. Click &lt;strong&gt;Create policy&lt;/strong&gt; to add a new policy.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;App sign-in policies page with the Create policy button&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-62.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;In the modal window, provide a &lt;strong&gt;Policy name&lt;/strong&gt; and &lt;strong&gt;Description&lt;/strong&gt;, then click &lt;strong&gt;Create Policy&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Create policy modal with the policy name and description&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-63.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;You will be redirected to your policy page. Click &lt;strong&gt;Add rule.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Authentication policy page with the Add rule button&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-64.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;A modal window will appear.
Enter &lt;strong&gt;rule name&lt;/strong&gt;, select the &lt;strong&gt;Groups&lt;/strong&gt; you want to enforce MFA for.Configure other fields as shown in the screenshots below. Then click &lt;strong&gt;Save&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Add rule modal with the rule name and the groups to enforce MFA for&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-65.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Add rule modal authentication settings&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-66.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Remaining Add rule settings with the Save button&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-67.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;Your new authentication policy rule will be added.&lt;/p&gt;
&lt;h3 id=&quot;configuring-authenticators&quot;&gt;Configuring authenticators&lt;a href=&quot;#configuring-authenticators&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Configuring authenticators&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In the side menu, navigate to the &lt;strong&gt;Authenticators&lt;/strong&gt; page and click on &lt;strong&gt;Enrollment&lt;/strong&gt; tab:&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Authenticators page with the Enrollment tab&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-68.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;Then click &lt;strong&gt;Actions&lt;/strong&gt; next to the default policy and select &lt;strong&gt;Edit&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Actions menu on the default enrollment policy with Edit selected&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-69.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;In the modal window that appears, ensure that &lt;strong&gt;Okta Verify&lt;/strong&gt; is set to &lt;strong&gt;Required&lt;/strong&gt;, then click Save.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Edit enrollment policy modal with Okta Verify set to Required&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-70.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;✅ That’s it!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Okta Multi-Factor Authentication (MFA) is now successfully configured.&lt;/p&gt;
&lt;h2 id=&quot;how-to-connect-to-your-application&quot;&gt;How to connect to your application&lt;a href=&quot;#how-to-connect-to-your-application&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: How to connect to your application&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To enable &lt;strong&gt;Okta login&lt;/strong&gt; for your users in &lt;strong&gt;your application&lt;/strong&gt;, you need to retrieve the following three parameters:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Client ID&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Client Secret&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Okta domain&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You can find this data on our Application details page. In the side menu (Okta panel), navigate to the &lt;strong&gt;Applications&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Choose an application which you created previously. In my case, it is an SSO OIDC (marked below)&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Applications page with the SSO OIDC application highlighted&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-71.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;Instructions for copying the whole information that we should fill in your application can see below&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;SSO OIDC application page showing the client ID and client secret&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-72.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Application settings showing the Okta domain&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-73.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;copy-a-issuer-url&quot;&gt;Copy a issuer URL&lt;a href=&quot;#copy-a-issuer-url&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Copy a issuer URL&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;To obtain the Issuer URL for your application, open: &lt;strong&gt;https://{yourOktaDomain}/.well-known/openid-configuration&lt;/strong&gt; (In my case it is: &lt;a href=&quot;https://integrator-1609932.okta.com/.well-known/openid-configuration&quot;&gt;https://integrator-1609932.okta.com/.well-known/openid-configuration&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;Then copy the value of the &lt;strong&gt;issuer&lt;/strong&gt; field from the returned JSON:&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;OpenID configuration JSON with the issuer field highlighted&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-74.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;✅ &lt;strong&gt;Now, you have all the required credentials to connect to your application!&lt;/strong&gt;&lt;/p&gt;
</content:encoded><category>okta</category><category>sso</category><category>authentication</category><category>security</category></item><item><title>Azure single sign-on (SSO) setup: a step-by-step guide</title><link>https://serhiisoldatov.dev/blog/azure-sso-setup/</link><guid isPermaLink="true">https://serhiisoldatov.dev/blog/azure-sso-setup/</guid><description>How to register an app in Azure (Microsoft Entra ID) for OIDC SSO: scopes, group claims, API permissions, app roles, groups, user soft-deletion, and optional MFA.</description><pubDate>Sat, 11 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;To enable Single Sign-On (SSO) for your application, we first need to register it in &lt;strong&gt;Azure Active&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Navigate to Microsoft Entra ID:&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Azure portal with Microsoft Entra ID&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-1.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;Click to &lt;strong&gt;Add&lt;/strong&gt; button and select &lt;strong&gt;App registration&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Microsoft Entra ID Add menu with App registration selected&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-2.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;Fill in the Name field (you can choose any meaningful name).&lt;/p&gt;
&lt;p&gt;Set the &lt;strong&gt;Redirect URI&lt;/strong&gt;, which look like this:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;https://your-user-pool-domain/oauth2/idpresponse &lt;/code&gt;&lt;/p&gt;
&lt;p&gt;where &lt;em&gt;your-user-pool-domain&lt;/em&gt; is your Cognito User Pool Domain&lt;/p&gt;
&lt;p&gt;(example: &lt;code&gt;https://eu-north-asr1mv.auth.eu-north-1.amazoncognito.com/oauth2/idpresponse&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Register an application form with the name and redirect URI&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-3.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;Other fields of this form fill as shown in the following screenshot:&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Remaining app registration fields filled in&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-4.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;configuring-logout-url--enabling-id-tokens&quot;&gt;Configuring logout URL &amp;amp; enabling ID tokens&lt;a href=&quot;#configuring-logout-url--enabling-id-tokens&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Configuring logout URL &amp;amp; enabling ID tokens&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To complete the authentication setup, we need to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Enable ID Tokens&lt;/strong&gt; - Since we are using &lt;strong&gt;OpenID Connect (OIDC)&lt;/strong&gt; to authenticate users, ID tokens must be enabled.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Set the Logout URL&lt;/strong&gt; - This ensures users are properly signed out when logging out of our application.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;steps-to-configure&quot;&gt;Steps to configure:&lt;a href=&quot;#steps-to-configure&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Steps to configure:&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Go to &lt;strong&gt;Authentication&lt;/strong&gt; in the registered app:&lt;br&gt;
&lt;strong&gt;Your Azure Active Directory instance&lt;/strong&gt; -&amp;gt; &lt;strong&gt;App registrations&lt;/strong&gt; -&amp;gt; &lt;strong&gt;&amp;lt; YourApp &amp;gt;&lt;/strong&gt; -&amp;gt; &lt;strong&gt;Authentication&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Set the &lt;strong&gt;Logout URL&lt;/strong&gt; to:&lt;br&gt;
&lt;a href=&quot;https://your-app-domain/api/oidc/logout&quot;&gt;https://your-app-domain/api/oidc/logout&lt;/a&gt;/&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Authentication page with the logout URL set and ID tokens enabled&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-5.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;creating-scopes&quot;&gt;Creating scopes&lt;a href=&quot;#creating-scopes&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Creating scopes&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;After creating the application, you will be redirected to the &lt;strong&gt;App Overview&lt;/strong&gt; page. Now, let’s configure scopes to define the permissions our application can request.&lt;/p&gt;
&lt;h3 id=&quot;step-1-navigate-to-expose-an-api&quot;&gt;Step 1: Navigate to Expose an API&lt;a href=&quot;#step-1-navigate-to-expose-an-api&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Step 1: Navigate to Expose an API&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Click on &lt;strong&gt;Expose an API&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;App registration sidebar with Expose an API selected&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-6.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;step-2-set-application-id-uri&quot;&gt;Step 2: Set application ID URI&lt;a href=&quot;#step-2-set-application-id-uri&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Step 2: Set application ID URI&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;If this is your first time accessing this section, you will see a prompt asking you to add an &lt;strong&gt;Application ID URI&lt;/strong&gt; before proceeding. This step is required to define unique identifiers for your API.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Follow the prompt to add an &lt;strong&gt;Application ID URI (generates automatically)&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Prompt to add an Application ID URI&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-7.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;step-3-add-a-group-scope&quot;&gt;Step 3: Add a group scope&lt;a href=&quot;#step-3-add-a-group-scope&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Step 3: Add a group scope&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Now that the &lt;strong&gt;Application ID URI&lt;/strong&gt; is set, we can define the scopes our application needs.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Click Add a Scope.&lt;/li&gt;
&lt;li&gt;Scopes in &lt;strong&gt;OIDC&lt;/strong&gt; determine what permissions the application can request from the identity provider. They allow access to specific user attributes, such as groups, roles, email, or profile information.&lt;/li&gt;
&lt;li&gt;In our case, we need the &lt;strong&gt;group&lt;/strong&gt; scope to retrieve user groups during authentication. This enables proper access control and permission management in our application.&lt;/li&gt;
&lt;li&gt;Fill this form as shown in the following screenshot, then click the &lt;strong&gt;Add scope&lt;/strong&gt; button.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Add a scope form filled in for the groups scope&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-8.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;step-31-add-a-roles-scope&quot;&gt;Step 3.1: Add a roles scope&lt;a href=&quot;#step-31-add-a-roles-scope&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Step 3.1: Add a roles scope&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Add a Scope&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Scopes in &lt;strong&gt;OIDC&lt;/strong&gt; determine what permissions the application can request from the identity provider. They allow access to specific user attributes, such as roles, email, or profile information.&lt;/li&gt;
&lt;li&gt;In our case, we need the &lt;strong&gt;roles&lt;/strong&gt; scope to retrieve user roles (e.g., admin, manager) during authentication. This enables proper access control and permission management in our application.&lt;/li&gt;
&lt;li&gt;Fill this form as shown in the following screenshot, then click the &lt;strong&gt;Add scope&lt;/strong&gt; button&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Add a scope form filled in for the roles scope&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-9.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;step-4-add-a-client-application&quot;&gt;Step 4: Add a client application&lt;a href=&quot;#step-4-add-a-client-application&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Step 4: Add a client application&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Once the scope is created, we need to associate it with our client application.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Add a client application&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;Client ID&lt;/strong&gt; can be found in the &lt;strong&gt;Application ID URI&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;With these steps completed, our application is now set up to request and utilize user groups during authentication.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Add a client application form with the client ID and authorized scopes&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-10.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;step-5-add-a-groups-to-token-configuration&quot;&gt;Step 5: Add a groups to token configuration&lt;a href=&quot;#step-5-add-a-groups-to-token-configuration&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Step 5: Add a groups to token configuration&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;After all is created we must create group claim&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Add group claim&lt;/strong&gt; button.&lt;/li&gt;
&lt;li&gt;Fill the form as shown in the following screenshot, then click the &lt;strong&gt;Save&lt;/strong&gt; button.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Token configuration with the Add group claim form&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-11.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;That’s it, &lt;strong&gt;the group’s&lt;/strong&gt; scope and claim is configured.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;extending-api-permissions&quot;&gt;Extending API permissions&lt;a href=&quot;#extending-api-permissions&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Extending API permissions&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Retrieving the &lt;strong&gt;email&lt;/strong&gt;, &lt;strong&gt;openid&lt;/strong&gt;, and &lt;strong&gt;profile&lt;/strong&gt; claims in the ID token, as well as creating notification subscriptions for soft delete support, requires enabling the appropriate &lt;strong&gt;API permissions&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Navigate to&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Your Azure Active Directory instance -&amp;gt; App registrations -&amp;gt; &amp;lt; YourApp &amp;gt; -&amp;gt; API Permissions&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Click &lt;strong&gt;Add permission&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;API permissions page with the Add permission button&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-12.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In the sidebar that appears, select &lt;strong&gt;APIs my organization uses&lt;/strong&gt;, then choose &lt;strong&gt;Microsoft Graph&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Request API permissions panel with Microsoft Graph under APIs my organization uses&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-13.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Choose &lt;strong&gt;Delegated permissions&lt;/strong&gt;, then enable: &lt;strong&gt;email&lt;/strong&gt;, &lt;strong&gt;openid&lt;/strong&gt;, &lt;strong&gt;profile&lt;/strong&gt;, &lt;strong&gt;User.Read&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Delegated permissions with email, openid and profile selected&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-14.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Delegated permissions with User.Read selected&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-15.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Choose &lt;strong&gt;Application permissions&lt;/strong&gt;, then enable: &lt;strong&gt;Directory.Read.All&lt;/strong&gt; and &lt;strong&gt;User.ReadAll&lt;/strong&gt;. Then Click on the &lt;strong&gt;Add permissions&lt;/strong&gt; button.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Application permissions with Directory.Read.All selected&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-16.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Application permissions with User.Read.All selected and the Add permissions button&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-17.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;this-is-what-the-correct-configuration-should-look-like&quot;&gt;This is what the correct configuration should look like:&lt;a href=&quot;#this-is-what-the-correct-configuration-should-look-like&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: This is what the correct configuration should look like:&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;API permissions page showing the configured Microsoft Graph permissions&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-18.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;creating-roles&quot;&gt;Creating roles&lt;a href=&quot;#creating-roles&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Creating roles&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In &lt;strong&gt;Azure Active Directory (AAD)&lt;/strong&gt;, we can define roles for users to manage permissions within our application. These roles determine the level of access a user has, such as &lt;strong&gt;admin&lt;/strong&gt;, &lt;strong&gt;manager&lt;/strong&gt;, etc.&lt;br&gt;
In the Azure Active Directory we should create roles for users.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Access the Manifest&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To create roles, we need to edit the &lt;strong&gt;application’s Manifest&lt;/strong&gt;, which is a JSON file defining the application’s configuration.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Navigate to:&lt;br&gt;
&lt;strong&gt;Your Azure Active Directory instance -&amp;gt; App registrations -&amp;gt; &amp;lt; YourApp &amp;gt; -&amp;gt; Manifest&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;App manifest editor with the empty appRoles section highlighted&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-19.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;In the &lt;strong&gt;Manifest&lt;/strong&gt;, you will find a section for role definitions. If you haven’t created any roles yet, this section will be empty (highlighted in red in the screenshot).&lt;/p&gt;
&lt;p&gt;Example of one role:&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;json&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;{&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    &quot;allowedMemberTypes&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; [&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;        &quot;User&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    ]&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    &quot;description&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;Administrators can manage resources.&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    &quot;displayName&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;Admin&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    &quot;id&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;a1234567-89ab-cdef-0123-456789abcdea&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    &quot;isEnabled&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; true&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    &quot;origin&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;Application&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    &quot;value&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;admin&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;}   &lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Each role must have a &lt;strong&gt;unique ID&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Use a &lt;strong&gt;GUID generator&lt;/strong&gt; to create unique IDs. You can use this free tool:&lt;br&gt;
👉&lt;a href=&quot;https://www.guidgenerator.com/&quot;&gt;GUID Generator&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;step-2-add-all-required-roles&quot;&gt;Step 2: Add all required roles&lt;a href=&quot;#step-2-add-all-required-roles&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Step 2: Add all required roles&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;For convenience, you can copy and paste the pre-defined role objects into your &lt;strong&gt;Manifest&lt;/strong&gt;.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Copy the prepared role definitions.&lt;/li&gt;
&lt;li&gt;Replace the role IDs with newly generated GUIDs.&lt;/li&gt;
&lt;li&gt;Press the “Save” button to apply the changes.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Once saved, your roles should appear correctly in the Manifest.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Here’s prepared user roles, you can just copy-paste the whole object, BUT do not forget to use your own ids by using GUID, after pasting press “Save” button:&lt;/em&gt;&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;json&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;appRoles&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;: [&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    {&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        &quot;allowedMemberTypes&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; [&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;            &quot;User&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;        ]&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        &quot;description&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;Full admin access.&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        &quot;displayName&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;ADMIN&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        &quot;id&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;18846fd3-94e1-4f47-a8e5-27edb66a14b8&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        &quot;isEnabled&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; true&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        &quot;origin&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;Application&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        &quot;value&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;admin&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    }&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    {&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        &quot;allowedMemberTypes&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; [&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;            &quot;User&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;        ]&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        &quot;description&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;Booking and campaign management access.&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        &quot;displayName&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;MANAGER&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        &quot;id&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;b2345678-90ab-cdef-0123-456789abcdeb&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        &quot;isEnabled&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; true&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        &quot;origin&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;Application&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        &quot;value&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;manager&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    }&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    {&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        &quot;allowedMemberTypes&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; [&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;            &quot;User&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;        ]&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        &quot;description&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;Booking and campaign management access.&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        &quot;displayName&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;REPORT&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        &quot;id&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;cd4062ff-2eb9-4960-93c1-1d20637f1d23&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        &quot;isEnabled&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; true&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        &quot;origin&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;Application&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        &quot;value&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;report&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    }&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;Here’s how it should look like:&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;App manifest with the admin, manager and report roles added&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-20.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;creating-groups&quot;&gt;Creating groups&lt;a href=&quot;#creating-groups&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Creating groups&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In &lt;strong&gt;Azure Active Directory (AAD)&lt;/strong&gt;, we can define groups for users to manage permissions within our application. Groups allow us to control which users have access to the app. A user must be assigned to the appropriate group in order to sign in.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;!Note&lt;/strong&gt;: If a user is not a member of the group, they will not be able to access your application.&lt;/p&gt;
&lt;h3 id=&quot;step-1-navigate-to-groups&quot;&gt;Step 1: Navigate to groups&lt;a href=&quot;#step-1-navigate-to-groups&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Step 1: Navigate to groups&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;To create groups:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Navigate to:&lt;br&gt;
&lt;strong&gt;Your Azure Active Directory instance -&amp;gt; Groups Manifest&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;You should see a screen similar to the one below:&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;“New group”&lt;/strong&gt; button.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Groups page with the New group button&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-21.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;step-2-create-group&quot;&gt;Step 2: Create group&lt;a href=&quot;#step-2-create-group&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Step 2: Create group&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;To create groups:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Choose &lt;strong&gt;Security&lt;/strong&gt; type in Group type input&lt;/li&gt;
&lt;li&gt;Fill other fields&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;“Create”&lt;/strong&gt; button. And &lt;strong&gt;Security group&lt;/strong&gt; will be created&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;New group form with the Security group type selected&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-22.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;step-3-assign-users-to-group&quot;&gt;Step 3: Assign users to group&lt;a href=&quot;#step-3-assign-users-to-group&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Step 3: Assign users to group&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;To assign users to group, follow these steps:&lt;/p&gt;
&lt;h3 id=&quot;step-1-navigate-to-group&quot;&gt;Step 1: Navigate to group&lt;a href=&quot;#step-1-navigate-to-group&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Step 1: Navigate to group&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Go to:&lt;br&gt;
&lt;strong&gt;Your Azure Active Directory instance -&amp;gt; Groups -&amp;gt; All groups&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Click on the &lt;strong&gt;Group&lt;/strong&gt; to which you want to add users.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;All groups list with the group to add users to&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-23.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;step-2-navigate-to-members-tab&quot;&gt;Step 2: Navigate to members tab&lt;a href=&quot;#step-2-navigate-to-members-tab&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Step 2: Navigate to members tab&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Click the &lt;strong&gt;“Members”&lt;/strong&gt; button on the sidebar.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Group sidebar with the Members tab&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-24.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;step-3-add-members-to-the-groups&quot;&gt;Step 3: Add members to the groups&lt;a href=&quot;#step-3-add-members-to-the-groups&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Step 3: Add members to the groups&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Add members&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Select the users you want to add to the group as on the screenshot below.&lt;/li&gt;
&lt;li&gt;After you select all users you want to add to the group. Click Select&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Add members panel with users selected&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-25.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;assigning-user-roles&quot;&gt;Assigning user roles&lt;a href=&quot;#assigning-user-roles&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Assigning user roles&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To assign roles to users, follow these steps:&lt;/p&gt;
&lt;h3 id=&quot;step-1-navigate-to-user-role-assignment&quot;&gt;Step 1: Navigate to user role assignment&lt;a href=&quot;#step-1-navigate-to-user-role-assignment&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Step 1: Navigate to user role assignment&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Go to:&lt;br&gt;
&lt;strong&gt;Your Azure Active Directory instance -&amp;gt; Enterprise applications -&amp;gt; &amp;lt; YourApp &amp;gt; -&amp;gt; Users and groups&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;“Add user”&lt;/strong&gt; button.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Enterprise application Users and groups page with the Add user button&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-26.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;step-2-select-users-and-assign-roles&quot;&gt;Step 2: Select users and assign roles&lt;a href=&quot;#step-2-select-users-and-assign-roles&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Step 2: Select users and assign roles&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;First, select the users you want to assign roles to.&lt;br&gt;
(See Screenshot below)&lt;/li&gt;
&lt;li&gt;On the next screen, choose the appropriate role for the user.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Assign&lt;/strong&gt; button&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Add assignment screen for selecting users&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-27.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Selecting a role for the assigned users&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-28.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;For production environments, it’s &lt;strong&gt;not recommended&lt;/strong&gt; to assign roles to individual users. Instead, assign roles to &lt;strong&gt;groups&lt;/strong&gt; to simplify user management and reduce maintenance efforts.&lt;/p&gt;
&lt;h3 id=&quot;step-3-confirm-role-assignment&quot;&gt;Step 3: Confirm role assignment&lt;a href=&quot;#step-3-confirm-role-assignment&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Step 3: Confirm role assignment&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Click the &lt;strong&gt;“Assign”&lt;/strong&gt; button to finalize the role assignment.&lt;/li&gt;
&lt;li&gt;You will be redirected to the &lt;strong&gt;Users and groups&lt;/strong&gt; view, where you can see the assigned roles.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Users and groups view showing the assigned roles&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-29.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;
That’s it! The Role assignment is now complete.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Now you have completed all preparation steps and your application is ready to use Azure SSO.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;deleting-users&quot;&gt;Deleting users&lt;a href=&quot;#deleting-users&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Deleting users&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To trigger a &lt;strong&gt;soft-delete&lt;/strong&gt; of a user in your application, you need to either remove the user from an Azure group or fully delete the user from Azure IAM.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Delete user from group:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To soft-delete a user from all rights holders where the user was, you need to remove the user from the corresponding Azure AD group (the group with the objectId defined in your SSO Client Provider).&lt;/p&gt;
&lt;h3 id=&quot;step-1-navigate-to-group-1&quot;&gt;Step 1: Navigate to group&lt;a href=&quot;#step-1-navigate-to-group-1&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Step 1: Navigate to group&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Go to:&lt;br&gt;
&lt;strong&gt;Your Azure Active Directory instance -&amp;gt; Groups -&amp;gt; All groups&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Click on the &lt;strong&gt;Group&lt;/strong&gt; to which you want to delete users.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;All groups list with the group to remove users from&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-30.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;step-2-navigate-to-members-tab-1&quot;&gt;Step 2: Navigate to members tab&lt;a href=&quot;#step-2-navigate-to-members-tab-1&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Step 2: Navigate to members tab&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Click the &lt;strong&gt;“Members”&lt;/strong&gt; button on the sidebar.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Group sidebar with the Members tab&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-31.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;step-3-remove-members-from-the-groups&quot;&gt;Step 3: Remove members from the groups&lt;a href=&quot;#step-3-remove-members-from-the-groups&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Step 3: Remove members from the groups&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Select the users you want to remove from the group.&lt;/li&gt;
&lt;li&gt;After you select all users you want to delete from the group. Click &lt;strong&gt;Remove&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;A modal window will appear with the configuration form. Click &lt;strong&gt;Yes&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Group members list with users selected and the Remove button&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-32.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Confirmation dialog for removing group members&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-33.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;After this, if a member with this ObjectID exists in your application, the system will &lt;strong&gt;soft-delete&lt;/strong&gt; the user.&lt;/p&gt;
&lt;p&gt;Delete user from IAM:&lt;/p&gt;
&lt;h3 id=&quot;step-1-navigate-to-users-page&quot;&gt;Step 1: Navigate to users page&lt;a href=&quot;#step-1-navigate-to-users-page&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Step 1: Navigate to users page&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Go to:&lt;br&gt;
&lt;strong&gt;Your Azure Active Directory instance -&amp;gt; Users&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Select the users you want to delete.&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;“Delete”&lt;/strong&gt; button, then confirm by clicking &lt;strong&gt;“Ok”&lt;/strong&gt; in the modal window.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Users page with a user selected and the Delete button&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-34.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Confirmation dialog for deleting the user&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-35.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;step-2-navigate-to-deleted-users-tab&quot;&gt;Step 2: Navigate to deleted users tab&lt;a href=&quot;#step-2-navigate-to-deleted-users-tab&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Step 2: Navigate to deleted users tab&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Click the &lt;strong&gt;“Deleted users”&lt;/strong&gt; button on the sidebar.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Users sidebar with the Deleted users tab&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-36.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;step-3-permanently-delete-the-user&quot;&gt;Step 3: Permanently delete the user&lt;a href=&quot;#step-3-permanently-delete-the-user&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Step 3: Permanently delete the user&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Select the user you delete in &lt;strong&gt;Step 1&lt;/strong&gt; or you want to delete.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;“Delete permanently”&lt;/strong&gt;, then confirm by clicking &lt;strong&gt;“Ok”&lt;/strong&gt; in the modal window.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Deleted users list with the Delete permanently button&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-37.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Confirmation dialog for permanently deleting the user&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-38.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;After this, the selected users will be completely deleted from Azure IAM.&lt;br&gt;
If a user with the same &lt;strong&gt;User ObjectID&lt;/strong&gt; exists in your application, that user will also be &lt;strong&gt;soft-deleted&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The User ObjectID is linked to a user in your application only after the user logs in via the Azure SSO Provider.&lt;/p&gt;
&lt;h2 id=&quot;optional-multi-factor-authentication&quot;&gt;(Optional) Multi factor authentication&lt;a href=&quot;#optional-multi-factor-authentication&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: (Optional) Multi factor authentication&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Multi-Factor Authentication (MFA) adds an additional layer of security to user login. When enabled, users must verify their identity using a second factor, such as the &lt;strong&gt;Microsoft Authenticator app&lt;/strong&gt;, in addition to their password.&lt;/p&gt;
&lt;h3 id=&quot;step-1&quot;&gt;Step 1:&lt;a href=&quot;#step-1&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Step 1:&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Navigate to:&lt;br&gt;
&lt;strong&gt;Your Azure Active Directory instance -&amp;gt; Properties&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Make sure that Security Defaults are enabled. if not - enable it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Entra ID properties page with security defaults enabled&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-39.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;After enabling &lt;strong&gt;Security Defaults&lt;/strong&gt;, when a user from your organization attempts to log in, they will be prompted for MFA.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The user will need to use the &lt;strong&gt;Microsoft Authenticator app&lt;/strong&gt; (or another supported MFA method) to complete the login.&lt;/li&gt;
&lt;li&gt;A screen similar to the one below will appear, guiding the user through the MFA verification process:&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Sign-in prompt asking the user to set up MFA with Microsoft Authenticator&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-40.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;On next login attempts, the user will be required to complete &lt;strong&gt;MFA&lt;/strong&gt; using the Microsoft Authenticator app.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;how-to-connect-your-application&quot;&gt;How to connect your application&lt;a href=&quot;#how-to-connect-your-application&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: How to connect your application&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To enable &lt;strong&gt;Azure login&lt;/strong&gt; for your users, you need to retrieve the following three parameters:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Client ID&lt;/li&gt;
&lt;li&gt;Client Secret&lt;/li&gt;
&lt;li&gt;Tenant ID&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;step-1-retrieve-client-id--tenant-id&quot;&gt;Step 1: Retrieve client ID &amp;amp; tenant ID&lt;a href=&quot;#step-1-retrieve-client-id--tenant-id&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Step 1: Retrieve client ID &amp;amp; tenant ID&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Navigate to:&lt;br&gt;
&lt;strong&gt;Your Azure Active Directory instance -&amp;gt; App registrations -&amp;gt; &amp;lt; YourApp &amp;gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Locate the &lt;strong&gt;Client ID&lt;/strong&gt; and &lt;strong&gt;Tenant ID&lt;/strong&gt; in the &lt;strong&gt;App Overview section&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;App overview showing the client ID, tenant ID and Client credentials link&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-41.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;step-2-generate-client-secret&quot;&gt;Step 2: Generate client secret&lt;a href=&quot;#step-2-generate-client-secret&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Step 2: Generate client secret&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Click on &lt;strong&gt;Client credentials&lt;/strong&gt; (as shown in Screenshot above).&lt;/li&gt;
&lt;li&gt;Create a new &lt;strong&gt;Client Secret&lt;/strong&gt;.&lt;br&gt;
(See Screenshot below)&lt;/li&gt;
&lt;li&gt;After creating the secret, &lt;strong&gt;copy the “Value” immediately&lt;/strong&gt;, as it will no longer be visible once you refresh or leave the page.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Certificates and secrets page with a new client secret being added&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-42.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;After creation you should copy Value&lt;br&gt;
Note! after updating page it won’t be visible anymore&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Client secret list with the Value column to copy&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-43.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;copy-a-issuer-url&quot;&gt;Copy a issuer URL&lt;a href=&quot;#copy-a-issuer-url&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Copy a issuer URL&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Reference:&lt;a href=&quot;https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc#find-your-apps-openid-configuration-document-uri&quot;&gt; &lt;/a&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc#find-your-apps-openid-configuration-document-uri&quot;&gt;https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc#find-your-apps-openid-configuration-document-uri&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;To obtain the Issuer URL for your application, copy: &lt;a href=&quot;https://login.microsoftonline.com/%7Btenant%7D/v2.0&quot;&gt;https://login.microsoftonline.com/{tenant}/v2.0&lt;/a&gt; and replace &lt;strong&gt;{tenant}&lt;/strong&gt; with your application’s &lt;strong&gt;tenant ID&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;OpenID configuration document with the issuer field&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-44.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;✅ Now you have all the required credentials to configure Azure SSO for your application!&lt;/p&gt;
</content:encoded><category>azure</category><category>sso</category><category>authentication</category><category>oidc</category></item><item><title>Django without the mess: repositories for data, services for rules</title><link>https://serhiisoldatov.dev/blog/django-repositories-and-services/</link><guid isPermaLink="true">https://serhiisoldatov.dev/blog/django-repositories-and-services/</guid><description>Why selectors are a trap for cleaning up fat Django models, and how repositories for data access plus services for business rules keep the layers apart.</description><pubDate>Sun, 07 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;the-elephant-in-the-room-djangos-fat-model-problem&quot;&gt;The elephant in the room: Django’s fat model problem&lt;a href=&quot;#the-elephant-in-the-room-djangos-fat-model-problem&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: The elephant in the room: Django’s fat model problem&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Let’s talk about the elephant in every Django project: that one &lt;code&gt;models.py&lt;/code&gt; file. You know the one. It started pure and simple, a beautiful reflection of your database schema. Now, it’s a 2,000-line behemoth, bloated with custom managers, &lt;code&gt;@property&lt;/code&gt; decorators that hide monstrous queries, and &lt;code&gt;save()&lt;/code&gt; methods that seem to have more side effects than a late-night infomercial product.&lt;/p&gt;
&lt;p&gt;Your views are a mess, your business logic is scattered, and making any change feels like performing surgery with a butter knife. In this moment of desperation, a seemingly elegant solution appears: “selectors.” Just pull all those messy queries out into a separate &lt;code&gt;selectors.py&lt;/code&gt; file! It feels clean. It feels right.&lt;/p&gt;
&lt;p&gt;But I’m here to tell you that selectors are a trap. They’re a quick fix that feels like progress but ultimately leads you down a path of architectural pain. There’s a better way, a more structured approach that will save you from future headaches: the Repository and Service patterns. Let’s break down why this combination wins, and why selectors just don’t hold up in the long run.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;how-we-all-got-here&quot;&gt;How we all got here&lt;a href=&quot;#how-we-all-got-here&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: How we all got here&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;No one sets out to write messy code. We typically arrive here by following well-intentioned advice. In the Django world, the classic mantra is “fat models, thin views.” The idea is to keep your views lean and push all the logic related to your data into the corresponding model.&lt;/p&gt;
&lt;p&gt;And for a small project, this works beautifully! A &lt;code&gt;User&lt;/code&gt; model with a method like &lt;code&gt;is_profile_complete()&lt;/code&gt; is perfectly reasonable. But what happens when the logic gets more complex? Soon, you have &lt;code&gt;user.get_recent_orders()&lt;/code&gt;, &lt;code&gt;user.calculate_lifetime_value()&lt;/code&gt;, and &lt;code&gt;user.send_password_reset_email()&lt;/code&gt;. Your model is no longer just a data structure; it’s a tangled web of business rules, database queries, and external interactions.&lt;/p&gt;
&lt;p&gt;This is when developers, rightly, look for a way to clean up. The &lt;code&gt;selectors.py&lt;/code&gt; file is born. You create functions like &lt;code&gt;user_get_active_with_recent_orders()&lt;/code&gt; and &lt;code&gt;product_get_top_sellers()&lt;/code&gt;. The model slims down, the view calls a clean function—problem solved, right?&lt;/p&gt;
&lt;p&gt;Not quite. Honestly, this is like sweeping a pile of dirt under the rug. The room looks cleaner at a glance, but you haven’t actually dealt with the mess. You’ve just moved it. This separation is superficial, and as we’ll see, it creates a whole new set of problems.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;repository-pattern-101-your-datas-bodyguard&quot;&gt;Repository pattern 101: your data’s bodyguard&lt;a href=&quot;#repository-pattern-101-your-datas-bodyguard&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Repository pattern 101: your data’s bodyguard&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;So, what’s the alternative? Let’s start with the first piece of the puzzle: the &lt;strong&gt;Repository Pattern&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In the simplest terms, a repository is an abstraction layer that sits between your application’s business logic and your database. Its one and only job is to handle data access. Think of it as a specialized agent for a particular model. If you need to get users, save a user, or delete a user, you talk to the &lt;code&gt;UserRepository&lt;/code&gt;. You design it to serve your use cases, not to mirror every possible query.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Structure&lt;/strong&gt;:&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;your_app/&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;  users/&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;    models.py&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;    repositories/&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;      __init__.py&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;      users.py&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;    services/&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;      __init__.py&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;      users.py&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;And here’s what that &lt;code&gt;UserRepository&lt;/code&gt; might look like, inheriting from a &lt;code&gt;BaseRepository&lt;/code&gt; (like the one &lt;a href=&quot;https://gist.github.com/soldatov-ss/ad8a158b5e65308c952553bbe451fda9&quot;&gt;here&lt;/a&gt;):&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;python&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# your_app/users/repositories/users.py&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; your_app&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;apps&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;common&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;repositories &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; BaseRepository &lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; your_app&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;users&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;models &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; User&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; typing &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; Optional&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; UserRepository&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;BaseRepository&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;):&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; get_by_id&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt; user_id&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; int&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; Optional&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;User&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;]&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;filter&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(id&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;user_id).&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;first&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;()&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; get_by_email&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt; email&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; str&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; Optional&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;User&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;]&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;filter&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(email&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;email).&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;first&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;Why this matters&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Clear contract.&lt;/strong&gt; It provides a single, explicit interface for data operations. No more &lt;code&gt;User.objects.filter(...)&lt;/code&gt; scattered across 20 different files. You centralize your query logic in one place.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Centralized query intent.&lt;/strong&gt; If a flow needs special prefetch or annotations, you add a method and keep it consistent.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test seams.&lt;/strong&gt; You can pass a fake repository in tests. You can add caching or logging in one place without hunting through views.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-service-layer-also-known-as-the-brain&quot;&gt;The service layer, also known as the brain&lt;a href=&quot;#the-service-layer-also-known-as-the-brain&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: The service layer, also known as the brain&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If repositories are your data’s bodyguards, services are the brain of the operation. Services sit above repositories to orchestrate business workflows and enforce rules. They are where your transactions should begin and end. If a business rule changes—like “premium users get priority processing”—you want one, and only one, place to make that change.&lt;/p&gt;
&lt;p&gt;The best analogy is this: &lt;strong&gt;Repositories fetch the ingredients. Services follow the recipe to cook the meal.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A service doesn’t know &lt;em&gt;how&lt;/em&gt; the ingredients are fetched (that’s the repository’s job), and the view doesn’t know &lt;em&gt;how&lt;/em&gt; the meal is cooked. The view just says, “I’d like to order the ‘New User Registration’ please.”&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;python&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# users/services/users.py&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt; .&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;repositories &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; UserRepository&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# Assume a NotificationService exists elsewhere&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; notifications&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;services &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; NotificationService&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; UserService&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; __init__&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt; user_repo&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; UserRepository&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt; notification_service&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; NotificationService):&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;        self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;user_repo &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; user_repo&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;        self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;notification_service &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; notification_service&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; register_user&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt; email&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; str&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt; name&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; str&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; User:&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;        # Business rule: check if user already exists&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;user_repo&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;get_by_email&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(email):&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;            raise&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; ValueError&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;User with this email already exists.&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;        # Step 1: Create the user via the repository&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;        user &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;user_repo&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;create&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(email&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;email, name&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;name)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;        # Step 2: Orchestrate another action&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;        self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;notification_service&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;send_welcome_email&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(user.email)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; user&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;This is clean, transactional, and easy to follow.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-real-risk-of-selectors-a-slippery-slope&quot;&gt;The real risk of selectors: a slippery slope&lt;a href=&quot;#the-real-risk-of-selectors-a-slippery-slope&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: The real risk of selectors: a slippery slope&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;So if services are the brain, why not just use selectors for the data? The risk isn’t that selectors are inherently bad, but that they are a &lt;strong&gt;slippery slope&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;They lack a strong architectural boundary, making it too easy to add “just one little piece” of business logic. Over time, these small compromises turn your clean query file into a tangled, secondary business logic layer.&lt;/p&gt;
&lt;p&gt;The Repository and Service pattern prevents this by creating &lt;strong&gt;explicit guardrails&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Repositories&lt;/strong&gt; have one job: get data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Services&lt;/strong&gt; have one job: enforce business rules.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This clear separation makes the right path the easy path and prevents the slow decay of your architecture.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;admin-actions-celery-tasks-and-the-rest-of-the-crew&quot;&gt;Admin actions, celery tasks, and the rest of the crew&lt;a href=&quot;#admin-actions-celery-tasks-and-the-rest-of-the-crew&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Admin actions, celery tasks, and the rest of the crew&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Keep the same pattern everywhere. Admin actions call services. Celery tasks call services. Management commands call services. DRF viewsets and APIViews call services. One flow. One place for idempotency and retries. If a task runs twice, the service checks state and either skips or continues.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;wiring-it-together-__init__py-as-a-composition-root&quot;&gt;Wiring it together: &lt;code&gt;__init__.py&lt;/code&gt; as a composition root&lt;a href=&quot;#wiring-it-together-__init__py-as-a-composition-root&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Wiring it together: __init__.py as a composition root&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Once you have repositories and services, you still need to instantiate them somewhere. A clean way to handle this is to use the package’s &lt;code&gt;__init__.py&lt;/code&gt; as a &lt;strong&gt;composition root&lt;/strong&gt;: one file that wires dependencies together and exports ready-to-use singletons.&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;users/&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;  services/&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;    __init__.py   ← wires everything here&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;    users.py&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span&gt;    notifications.py&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;python&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# users/services/__init__.py&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt; .&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;notifications &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; NotificationService&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt; .&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;users &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; UserService&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; users&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;repositories &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; user_repository&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;notification_service &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; NotificationService&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;()&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;user_service &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; UserService&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;    user_repo&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;user_repository,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;    notification_service&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;notification_service,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt;__all__&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; [&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;user_service&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;notification_service&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;Now callers import a single, already-wired object:&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;python&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# instead of this&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; users&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;services&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;users &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; UserService&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;user_service &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; UserService&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(user_repo&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;..., notification_service&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;...)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# just this&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; users&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;services &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; user_service&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;This keeps instantiation in one place, hides the internal module structure, and makes the public API explicit via &lt;code&gt;__all__&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;One tradeoff to know:&lt;/strong&gt; services are instantiated at import time. If a test needs different wiring — say, a fake repository — it can’t swap the dependency before the module loads. The fix is &lt;code&gt;unittest.mock.patch&lt;/code&gt; on the singleton, or instantiating locally in the test. Neither is painful, but it’s worth knowing before you adopt the pattern.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;common-questions-you-will-hear-on-the-team&quot;&gt;Common questions you will hear on the team&lt;a href=&quot;#common-questions-you-will-hear-on-the-team&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Common questions you will hear on the team&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Can I keep a small helper on the model.&lt;/strong&gt; Yes, if it only touches that model’s state.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Do I always need a service.&lt;/strong&gt; If a view is a straight read, repo to serializer can be fine. When you branch or change state, reach for a service.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Do I need a repo per model.&lt;/strong&gt; Start where it helps most. Big aggregates, heavy queries, hot paths. Let the layer grow with need.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Can selectors live beside repos.&lt;/strong&gt; If they exist, make them thin wrappers over repos, not a separate source of truth.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How does this fit with DRF?&lt;/strong&gt; Your viewset or &lt;code&gt;APIView&lt;/code&gt; calls the service, then passes the result to a serializer. The service never touches the request object or serializer context. That boundary keeps your business logic framework-agnostic — the same service works from a Celery task or a management command without modification.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;why-this-matters-on-teams-not-only-in-code&quot;&gt;Why this matters on teams, not only in code&lt;a href=&quot;#why-this-matters-on-teams-not-only-in-code&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Why this matters on teams, not only in code&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Clean layers make it easier to talk. A PM says, payment failed on retry. You look at one place. A junior asks, where should I put this logic. You have a ready answer. A reviewer reads a PR and knows exactly which patterns to expect. The shape of the system stays steady as the people change.&lt;/p&gt;
&lt;p&gt;Also, repositories act like discreet places to teach performance. You can add little comments, like why you used &lt;code&gt;select_for_update&lt;/code&gt; here, or why an index matters. New teammates learn by seeing the same pattern in many methods. That is softer than sending a long wiki doc that nobody reads twice.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;conclusion-choose-clarity-over-convenience&quot;&gt;Conclusion: choose clarity over convenience&lt;a href=&quot;#conclusion-choose-clarity-over-convenience&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Conclusion: choose clarity over convenience&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Look, selectors aren’t born from malice. They come from a good place: the desire to clean up messy code. They offer a moment of relief, a feeling of organization. But it’s a temporary fix. They are a crutch, not a long-term architectural strategy.&lt;/p&gt;
&lt;p&gt;The Repository and Service patterns require a bit more discipline upfront. You have to think about your application’s layers and enforce those boundaries
Keep models slim. Put data access in repositories. Put rules in services. Own transactions at the service layer. Be consistent.&lt;/p&gt;
</content:encoded><category>django</category><category>python</category><category>architecture</category><category>clean-code</category></item><item><title>Django REST Framework: when (and when not) to override serializers and viewsets, part 2</title><link>https://serhiisoldatov.dev/blog/drf-when-to-override-part-2/</link><guid isPermaLink="true">https://serhiisoldatov.dev/blog/drf-when-to-override-part-2/</guid><description>Part 2 covers ViewSets: when to override create versus perform_create, how update, partial_update, and perform_update fit together, and a cheat sheet for where each kind of logic goes.</description><pubDate>Sun, 10 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;drf-part-2-viewset-overrides&quot;&gt;DRF, part 2: ViewSet overrides&lt;a href=&quot;#drf-part-2-viewset-overrides&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: DRF, part 2: ViewSet overrides&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Welcome to the second half of our guide on DRF architecture. In &lt;a href=&quot;/blog/drf-when-to-override-part-1/&quot;&gt;Part 1&lt;/a&gt;, we established a clear rule: serializers are the guardians of your data. They handle validation, transformation, and representation. They ensure that the data entering your system is clean and the data leaving is well-formed.&lt;/p&gt;
&lt;p&gt;But that’s only half the picture.&lt;/p&gt;
&lt;h2 id=&quot;the-viewsets-core-mission-orchestration&quot;&gt;The ViewSet’s core mission: orchestration&lt;a href=&quot;#the-viewsets-core-mission-orchestration&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: The ViewSet’s core mission: orchestration&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Viewsets handle the HTTP journey. They don’t care if a &lt;code&gt;start_date&lt;/code&gt; is before an &lt;code&gt;end_date&lt;/code&gt; — that’s the serializer’s job. They care about things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Parsing the incoming &lt;code&gt;request&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Checking permissions and authentication.&lt;/li&gt;
&lt;li&gt;Calling the serializer to validate data.&lt;/li&gt;
&lt;li&gt;Triggering the save operation.&lt;/li&gt;
&lt;li&gt;Formatting the final &lt;code&gt;Response&lt;/code&gt; with the right data and status code.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let’s explore the most common and powerful methods you can override.&lt;/p&gt;
&lt;h2 id=&quot;the-big-showdown-create-vs-perform_create&quot;&gt;The big showdown: &lt;code&gt;create&lt;/code&gt; vs. &lt;code&gt;perform_create&lt;/code&gt;&lt;a href=&quot;#the-big-showdown-create-vs-perform_create&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: The big showdown: create vs. perform_create&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This is where so much confusion happens. Both seem to do the same thing! But their purpose is completely different, and choosing the right one is key to clean code.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Override &lt;code&gt;create(self, request, *args, **kwargs)&lt;/code&gt; when you need to change the &lt;strong&gt;orchestration of the request or the shape of the final response&lt;/strong&gt;. Do you need to return a &lt;code&gt;202 Accepted&lt;/code&gt; instead of a &lt;code&gt;201 Created&lt;/code&gt;? Do you need to wrap the response in a &lt;code&gt;{ &quot;data&quot;: ... }&lt;/code&gt; envelope? Do you need to do something before the serializer is even validated? That’s a job for create.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Override &lt;code&gt;perform_create(self, serializer)&lt;/code&gt; when you just need to &lt;strong&gt;tweak how the object is saved&lt;/strong&gt;. This is a much cleaner, more focused hook. The default create method calls this after validation is successful. It’s the perfect place to inject the current user or tenant ID right before saving.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;A practical rule of thumb&lt;/strong&gt;: Always try to use &lt;code&gt;perform_create&lt;/code&gt; first. Only override the full &lt;code&gt;create&lt;/code&gt; method if you absolutely have to manipulate the HTTP response or the flow itself.&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;python&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# In your OrderViewSet&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; OrderViewSet&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;viewsets&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;ModelViewSet&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;):&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;    # ... queryset and serializer_class definitions ...&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;    # This is the CLEAN way to stamp the user&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; perform_create&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt; serializer&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;):&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;        # The serializer is already validated here.&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;        serializer&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;save&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(customer&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;self.request.user)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;    # Only override this if you need to change the whole dance&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; create&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt; request&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt;args&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; **&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt;kwargs&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;):&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;        # Maybe you need to check inventory before even trying to validate&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; not&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; check_inventory&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(request.data):&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;            return&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; Response&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;({&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;error&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;Item out of stock&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;}, status&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;status.HTTP_400_BAD_REQUEST)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;        # Now call the original &apos;create&apos; logic&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; super&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;().&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;create&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(request, &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;args, &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;**&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;kwargs)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;This principle of separating the HTTP-level orchestration (create) from the persistence hook (perform_create) is key. Now, let’s apply that same logic to updates.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-update-trinity-update-put-partial_update-patch-and-perform_update&quot;&gt;The update trinity: &lt;code&gt;update&lt;/code&gt; (PUT), &lt;code&gt;partial_update&lt;/code&gt; (PATCH), and &lt;code&gt;perform_update&lt;/code&gt;&lt;a href=&quot;#the-update-trinity-update-put-partial_update-patch-and-perform_update&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: The update trinity: update (PUT), partial_update (PATCH), and perform_update&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Updates are more complex than creates because there are two different ways to do them: a full update (&lt;code&gt;PUT&lt;/code&gt;) and a partial one (&lt;code&gt;PATCH&lt;/code&gt;). DRF gives you three distinct methods to control this process.&lt;/p&gt;
&lt;p&gt;First, let’s get the flow right. It’s a common point of confusion. The methods &lt;code&gt;update&lt;/code&gt; and &lt;code&gt;partial_update&lt;/code&gt; are peers; one does not call the other.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;A &lt;code&gt;PUT&lt;/code&gt; request is routed to the &lt;code&gt;update()&lt;/code&gt; method. After validation, &lt;code&gt;update()&lt;/code&gt; calls &lt;code&gt;perform_update()&lt;/code&gt; to save the changes.
&lt;strong&gt;Flow&lt;/strong&gt;: &lt;code&gt;PUT Request&lt;/code&gt; → &lt;code&gt;update()&lt;/code&gt; → &lt;code&gt;perform_update()&lt;/code&gt; → &lt;code&gt;serializer.save()&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A &lt;code&gt;PATCH&lt;/code&gt; request is routed to the &lt;code&gt;partial_update()&lt;/code&gt; method. After partial validation, &lt;code&gt;partial_update()&lt;/code&gt; also calls &lt;code&gt;perform_update()&lt;/code&gt; to save the changes.
&lt;strong&gt;Flow&lt;/strong&gt;: &lt;code&gt;PATCH Request&lt;/code&gt; → &lt;code&gt;partial_update()&lt;/code&gt; → &lt;code&gt;perform_update()&lt;/code&gt; → &lt;code&gt;serializer.save()&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;perform_update()&lt;/code&gt; is the shared, final step for both. With that in mind, here’s when to override each one.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;when-to-override-perform_updateself-serializer&quot;&gt;When to override &lt;code&gt;perform_update(self, serializer)&lt;/code&gt;&lt;a href=&quot;#when-to-override-perform_updateself-serializer&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: When to override perform_update(self, serializer)&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;This is your go-to hook for 90% of update logic. Override &lt;code&gt;perform_update&lt;/code&gt; when you have code that &lt;strong&gt;must run for any update, whether it’s a&lt;/strong&gt; &lt;code&gt;PUT&lt;/code&gt; &lt;strong&gt;or a&lt;/strong&gt; &lt;code&gt;PATCH&lt;/code&gt;. It keeps your code DRY (Don’t Repeat Yourself) by providing a single place for shared save-time logic.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Why Override?&lt;/strong&gt; You need to stamp a field, clear a cache, or log an update without caring if it was a full or partial change.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Example&lt;/strong&gt;: You always want to record which user was the last person to modify an object.&lt;/li&gt;
&lt;/ul&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;python&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; perform_update&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt; serializer&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;):&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;    # This logic runs for both PUT and PATCH, after validation.&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;    # It&apos;s the cleanest place for shared save-time hooks.&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    serializer&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;save&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(last_updated_by&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;self.request.user)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;h3 id=&quot;when-to-override-partial_updateself-request-args-kwargs&quot;&gt;When to override &lt;code&gt;partial_update(self, request, *args, **kwargs)&lt;/code&gt;&lt;a href=&quot;#when-to-override-partial_updateself-request-args-kwargs&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: When to override partial_update(self, request, *args, **kwargs)&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Override the &lt;code&gt;partial_update&lt;/code&gt; method for logic that should &lt;strong&gt;only run during a partial change&lt;/strong&gt; (&lt;code&gt;PATCH&lt;/code&gt;). This is incredibly useful for implementing fine-grained permissions or triggering specific side effects when certain fields are modified&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Why Override?&lt;/strong&gt; You want to allow different update rules for different fields, or you want to react to specific, targeted changes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Example&lt;/strong&gt;: In a project management system, maybe anyone can &lt;code&gt;PATCH&lt;/code&gt; a task’s &lt;code&gt;description&lt;/code&gt;, but only a manager can &lt;code&gt;PATCH&lt;/code&gt; its &lt;code&gt;due_date&lt;/code&gt;. Overriding &lt;code&gt;partial_update&lt;/code&gt; is the perfect place to check for this.&lt;/li&gt;
&lt;/ul&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;python&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; partial_update&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt; request&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt;args&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; **&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt;kwargs&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;):&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;    # Custom logic just for PATCH&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &apos;due_date&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; in&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; request&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;data &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;and&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; not&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; request&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;user&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;is_manager&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; Response&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;({&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;error&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;Only managers can change the due date.&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;}, status&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;status.HTTP_403_FORBIDDEN)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;    # Another example: trigger a notification ONLY if the status changes&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &apos;status&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; in&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; request&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;data&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;        instance &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;get_object&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;()&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;        old_status &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; instance&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;status&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;        new_status &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; request&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;data&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&apos;status&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; old_status &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; new_status&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;            notify_team_of_status_change&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(instance, new_status)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; super&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;().&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;partial_update&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(request, &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;args, &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;**&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;kwargs)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;h3 id=&quot;when-to-override-updateself-request-args-kwargs&quot;&gt;When to override &lt;code&gt;update(self, request, *args, **kwargs)&lt;/code&gt;&lt;a href=&quot;#when-to-override-updateself-request-args-kwargs&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: When to override update(self, request, *args, **kwargs)&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Override the &lt;code&gt;update&lt;/code&gt; method when you need to add logic that is &lt;strong&gt;specific to a full resource replacement&lt;/strong&gt; (&lt;code&gt;PUT&lt;/code&gt;). Because &lt;code&gt;PUT&lt;/code&gt; implies replacing the entire resource, you might have preconditions or post-conditions that don’t apply to a simple &lt;code&gt;PATCH&lt;/code&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Why Override?&lt;/strong&gt; You want to enforce a strict “all or nothing” replacement policy or perform an action that only makes sense when the entire object is changed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Example&lt;/strong&gt;: Imagine you have a &lt;code&gt;Settings&lt;/code&gt; object. You could override &lt;code&gt;update&lt;/code&gt; to ensure that a &lt;code&gt;PUT&lt;/code&gt; request truly contains every single required setting field, preventing users from accidentally wiping out settings by sending an incomplete object. You could also log a specific “Settings Overwritten” event that is more severe than a simple field change.&lt;/li&gt;
&lt;/ul&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;python&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; update&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt; request&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt;args&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; **&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt;kwargs&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;):&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;    # Custom logic just for PUT&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; not&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; all&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(key &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt; request.data &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt; key &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&apos;theme&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&apos;notifications&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&apos;timezone&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;]):&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; Response&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;({&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;error&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;A full update requires all setting keys.&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;}, status&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;status.HTTP_400_BAD_REQUEST)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;    # Log the major change&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;    log_settings_overwritten&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(user&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;request.user)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; super&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;().&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;update&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(request, &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;args, &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;**&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;kwargs)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;hr&gt;
&lt;h2 id=&quot;other-useful-viewset-hooks&quot;&gt;Other useful ViewSet hooks&lt;a href=&quot;#other-useful-viewset-hooks&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Other useful ViewSet hooks&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Beyond &lt;code&gt;create&lt;/code&gt; and &lt;code&gt;update&lt;/code&gt;, other methods give you powerful control over the request lifecycle:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;list()&lt;/code&gt;: Override this when you need to reshape the entire list response beyond a simple array of objects, for example, by adding metadata or aggregation summaries.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;retrieve()&lt;/code&gt;: Perfect for when a single object’s response needs extra context that depends on the request, like adding a &lt;code&gt;can_edit&lt;/code&gt; flag for the current user.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;destroy()&lt;/code&gt;: The default just deletes the object. Override this to implement soft deletes, check for dependencies, or record an audit log event.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;get_queryset()&lt;/code&gt;: This is one of the most important overrides for performance and security. It’s where you scope the data (e.g., a user only sees their own invoices) and where you should add performance boosters like &lt;code&gt;select_related&lt;/code&gt; and &lt;code&gt;prefetch_related&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;get_serializer_class()&lt;/code&gt;: Your viewset’s “chameleon.” It lets you use different serializers for different actions (e.g., a summary for &lt;code&gt;list&lt;/code&gt;, details for &lt;code&gt;retrieve&lt;/code&gt;).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;get_permissions()&lt;/code&gt;: Great for when permissions change based on the action (e.g., anyone can &lt;code&gt;GET&lt;/code&gt;, but only staff can &lt;code&gt;POST&lt;/code&gt;).&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-decision-guide-where-does-this-logic-go&quot;&gt;The decision guide: where does this logic go?&lt;a href=&quot;#the-decision-guide-where-does-this-logic-go&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: The decision guide: where does this logic go?&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Let’s boil everything from both articles down to a simple cheat sheet for a clean DRF architecture:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Is it about the shape, structure, or validity of data?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Answer&lt;/strong&gt;: Serializer (&lt;code&gt;validate&lt;/code&gt;, &lt;code&gt;validate_&amp;lt;field&amp;gt;&lt;/code&gt;, &lt;code&gt;to_representation&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Is it about managing the HTTP request/response flow, like returning a custom status code or handling action-specific permissions?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Answer&lt;/strong&gt;: ViewSet action (&lt;code&gt;create&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt;, &lt;code&gt;list&lt;/code&gt;, etc.).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Is it about changing how an object is saved after validation, for both &lt;code&gt;PUT&lt;/code&gt; and &lt;code&gt;PATCH&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Answer&lt;/strong&gt;: ViewSet &lt;code&gt;perform_*&lt;/code&gt; method (&lt;code&gt;perform_create&lt;/code&gt;, &lt;code&gt;perform_update&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Is it about filtering the main list of objects based on the user or request?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Answer&lt;/strong&gt;: ViewSet &lt;code&gt;get_queryset&lt;/code&gt; method.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Keeping these boundaries clear isn’t just an abstract architectural exercise. It’s a practical strategy for building software that can grow and adapt without collapsing under its own weight. Your team will thank you. And more importantly, your future self will, too.&lt;/p&gt;
</content:encoded><category>django</category><category>drf</category><category>architecture</category><category>clean-code</category></item><item><title>Django REST Framework: when (and when not) to override serializers and viewsets, part 1</title><link>https://serhiisoldatov.dev/blog/drf-when-to-override-part-1/</link><guid isPermaLink="true">https://serhiisoldatov.dev/blog/drf-when-to-override-part-1/</guid><description>Part 1 covers serializers: which methods to override for validation, saving, and output formatting, and what logic does not belong in a serializer at all.</description><pubDate>Sun, 10 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;drf-part-1-serializer-overrides&quot;&gt;DRF, part 1: serializer overrides&lt;a href=&quot;#drf-part-1-serializer-overrides&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: DRF, part 1: serializer overrides&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Ask any Django developer where to put validation, formatting, and orchestration logic, and you’ll often get different answers — some say “just put it in the serializer,” others drop everything in the viewset.&lt;/p&gt;
&lt;p&gt;This works… until it doesn’t.
When your API grows, mixing concerns makes code brittle, hard to test, and painful to maintain. Knowing exactly &lt;strong&gt;when&lt;/strong&gt; to override a serializer method and &lt;strong&gt;when&lt;/strong&gt; to override a viewset method is key to keeping a codebase clean.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-serializers-core-mission&quot;&gt;The serializer’s core mission&lt;a href=&quot;#the-serializers-core-mission&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: The serializer’s core mission&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Think of your serializer as the security guard at the door of your database. It’s responsible for three primary tasks:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Validation&lt;/strong&gt;: Checking the ID of incoming data to ensure it’s legitimate.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transformation&lt;/strong&gt;: Making sure the data is in the right format before being saved.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Representation&lt;/strong&gt;: Deciding how the data should be presented when it’s sent back out.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Its world is small and focused: just the data. Let’s look at the tools it uses.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-serializer-method-overrides&quot;&gt;1) Serializer method overrides&lt;a href=&quot;#1-serializer-method-overrides&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: 1) Serializer method overrides&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Role&lt;/strong&gt;
Serializers control what may enter and how it leaves: validation, input normalization, output formatting.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;validate_&amp;lt;field_name&amp;gt;(self, value)&lt;/code&gt; - field-level checks
This is your go-to for single-field validation. Django’s model fields and DRF’s serializer fields handle the basics, like checking if a field is an integer or a valid email. But what about your specific business rules?&lt;/p&gt;
&lt;p&gt;Use this when a single field has a rule that only it needs to worry about.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;: Imagine you have a &lt;code&gt;Product&lt;/code&gt; model with a &lt;code&gt;discount_percentage&lt;/code&gt; field. For most products, any discount is fine. But for the “Electronics” category, you need to cap it at 50% to protect your margins.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;validate(self, data)&lt;/code&gt; - cross-field validation
Sometimes fields can’t be validated in isolation. They have relationships; they depend on each other. &lt;code&gt;validate&lt;/code&gt; is where your fields have a conversation. It runs after all the individual &lt;code&gt;validate_&amp;lt;field&amp;gt;&lt;/code&gt; methods have passed.&lt;/p&gt;
&lt;p&gt;Use this for cross-field validation.
&lt;strong&gt;Example&lt;/strong&gt;: A classic case is a &lt;code&gt;PromoCampaign&lt;/code&gt; model with a &lt;code&gt;start_date&lt;/code&gt; and an &lt;code&gt;end_date&lt;/code&gt;. It makes no sense for the promotion to end before it even begins.&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;python&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; validate&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt; data&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;):&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;    &quot;&quot;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;    Check that the start date is before the end date.&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;    &quot;&quot;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; data&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&apos;start_date&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;]&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; &amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; data&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&apos;end_date&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;]:&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        raise&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; serializers&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;ValidationError&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;End date must occur after start date.&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; data&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;Simple enough, right? This keeps your data integrity rules right next to the data definition.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;create(self, validated_data)&lt;/code&gt;
DRF’s default &lt;code&gt;create&lt;/code&gt; method is wonderfully simple: it just unpacks your validated data and calls &lt;code&gt;YourModel.objects.create(**validated_data)&lt;/code&gt;. But sometimes “simple” isn’t enough.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Override &lt;code&gt;create&lt;/code&gt; when you need to control exactly how a new object instance comes into being. This could mean:&lt;/li&gt;
&lt;li&gt;Creating related objects in the same transaction.&lt;/li&gt;
&lt;li&gt;Injecting data that doesn’t come from the user, like &lt;code&gt;created_by=self.context[&apos;request&apos;].user&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Handling nested serializers for write operations.&lt;/li&gt;
&lt;/ul&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;python&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# In a UserProfileSerializer that also creates a user&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; create&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt; validated_data&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;):&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    user_data &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; validated_data&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;pop&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&apos;user&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    user &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; User&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;objects&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;create_user&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;**&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;user_data)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;    # Stamp the current user from the view&apos;s context&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    created_by_user &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;context&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&apos;request&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;].&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;user&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    profile &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; UserProfile&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;objects&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;create&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(user&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;user, created_by&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;created_by_user, &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;**&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;validated_data)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; profile&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;&lt;code&gt;update(self, instance, validated_data)&lt;/code&gt;
Just like &lt;code&gt;create&lt;/code&gt;, the default &lt;code&gt;update&lt;/code&gt; method loops through the validated data and does a &lt;code&gt;setattr(instance, key, value)&lt;/code&gt; for each item before calling &lt;code&gt;instance.save()&lt;/code&gt;. You should override it when you have more complex update logic.&lt;/p&gt;
&lt;p&gt;Maybe you need to prevent updates if an order is already “shipped.” Or perhaps updating one field requires a calculated change to another.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;: When updating a blog post, you want to replace all its tags, not just add new ones. The default &lt;code&gt;update&lt;/code&gt; for a many-to-many relationship might not do exactly what you want.&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;python&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# In a PostSerializer with a &apos;tags&apos; field&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; update&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt; instance&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt; validated_data&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;):&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    tags_data &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; validated_data&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;pop&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&apos;tags&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;    # This is the default behavior for all other fields&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    instance &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; super&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;().&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;update&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(instance, validated_data)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;    # Now, handle the tags with our custom logic&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; tags_data &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;is&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; not&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; None&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;        instance&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;tags&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;set&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(tags_data)&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt; # Replaces all existing tags&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; instance&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;&lt;code&gt;to_representation(self, instance)&lt;/code&gt;
This is the “glow-up” method. It’s the last stop before your data is serialized and sent out into the world. Its job is to shape the outbound data. The model might store a user ID, but you want to show their full name. The database has a &lt;code&gt;first_name&lt;/code&gt; and &lt;code&gt;last_name&lt;/code&gt;, but you want to add a &lt;code&gt;full_name&lt;/code&gt; field to the API response.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;: Add a computed field to your User serializer.&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;python&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# In your UserSerializer&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; to_representation&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt; instance&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;):&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;    # Get the default representation&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    representation &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; super&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;().&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;to_representation&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(instance)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;    # Add our custom field&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    representation&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&apos;full_name&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;]&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; instance&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;get_full_name&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;()&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; representation&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;h3 id=&quot;so-when-should-serializers-just-say-no&quot;&gt;So, when should serializers just say no?&lt;a href=&quot;#so-when-should-serializers-just-say-no&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: So, when should serializers just say no?&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;This is just as important. A serializer that does too much becomes a god object. Here’s what doesn’t belong in a serializer:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Heavy Side Effects&lt;/strong&gt;: Sending emails, charging credit cards, updating inventory in another system, calling third-party APIs. This is a one-way ticket to a maintenance nightmare. If the database transaction fails after the email is sent, what do you do? This logic belongs elsewhere.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Complex Queries&lt;/strong&gt;: A serializer’s &lt;code&gt;validate&lt;/code&gt; method shouldn’t be making five different database calls to check a condition. That logic should live in a dedicated place (like a “selector” function or repository’s function) and be called from the view.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Business Workflows&lt;/strong&gt;: A multi-step process, like “register user, create trial subscription, and schedule a welcome email,” is a business workflow. It’s too high-level for a serializer. This is a job for a service layer.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;In &lt;a href=&quot;/blog/drf-when-to-override-part-2/&quot;&gt;Part 2&lt;/a&gt; of this series, we’ll dive into the ViewSet’s role as the orchestra’s conductor and explore how to use its methods—and a service layer—to keep your application logic clean and organized.&lt;/p&gt;
</content:encoded><category>django</category><category>drf</category><category>architecture</category><category>clean-code</category></item><item><title>Why Django REST Framework doesn&apos;t show your custom validation error messages (and what to do about it)</title><link>https://serhiisoldatov.dev/blog/drf-custom-validation-error-messages/</link><guid isPermaLink="true">https://serhiisoldatov.dev/blog/drf-custom-validation-error-messages/</guid><description>DRF validates at the serializer level and skips model full_clean(), so custom messages on model constraints never reach the API response. Three ways to work around it.</description><pubDate>Wed, 21 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The GitHub Discussion: &lt;a href=&quot;https://github.com/encode/django-rest-framework/discussions/7850&quot;&gt;link&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;GitHub comment by grjones from 2015 arguing that ModelSerializers no longer running model-level clean or full_clean pushes business logic into views&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-1.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;issue-developers-face&quot;&gt;Issue developers face:&lt;a href=&quot;#issue-developers-face&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Issue developers face:&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;When developing with Django REST Framework (DRF), developers typically run into issues where error messages for custom validation specified on Django model constraints (e.g., UniqueConstraint.violation_error_message) are not passed through to API responses. DRF returns default validation error messages instead.&lt;/p&gt;
&lt;p&gt;For example, a custom error message like:&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;python&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;UniqueConstraint&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;    fields&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&apos;email&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&apos;username&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;],&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;    violation_error_message&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&apos;This email and username combination already exists.&apos;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;…might never actually reach the API client.&lt;/p&gt;
&lt;p&gt;Instead, you’ll see something generic like:&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;json&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;&quot;non_field_errors&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;The fields email, username must make a unique set.&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;]}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;This is what the DRF maintainers had to say about it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;DRF validation is deliberately occurring at the serializer level, &lt;strong&gt;before&lt;/strong&gt; the creation of the model instance.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;It intentionally avoids calling Django’s &lt;code&gt;full_clean()&lt;/code&gt; in this processes, as serializers are meant to validate incoming data instead of an already-created model instance.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Due to the complexity and backward compatibility issues, there are no immediate plans from DRF for changing this behavior.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In other words - DRF prioritises serialiser checking for API input, which means that unless you explicitly call model checking (e.g. via &lt;code&gt;full_clean()&lt;/code&gt;), constraint errors and their custom messages will never be called or shown in API responses.&lt;/p&gt;
&lt;h2 id=&quot;possible-solutions&quot;&gt;Possible solutions:&lt;a href=&quot;#possible-solutions&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Possible solutions:&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Developers can tackle this issue through several practical approaches:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Explicitly calling &lt;code&gt;full_clean()&lt;/code&gt;:&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;python&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; create&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt; validated_data&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;):&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    instance &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; MyModel&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;**&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;validated_data)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    try&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;        instance&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;full_clean&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;()&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    except&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; DjangoValidationError &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; e&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        raise&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; DRFValidationError&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(e.message_dict)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    instance&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;save&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;()&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; instance&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Custom serializer validators:&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This method just overrides DRF’s built-in UniqueTogetherValidator just to replace the error message.&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;python&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; CustomUniqueValidator&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;UniqueTogetherValidator&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;):&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; __call__&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt; attrs&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-parameter)&quot;&gt; serializer&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;):&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        try&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt;            super&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;().&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;__call__&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(attrs, serializer)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;        except&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; serializers&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;ValidationError&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;            raise&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; serializers&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;ValidationError&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;This email and username combination already exists.&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sometimes just double your validation code:&lt;/strong&gt;
Sometimes, custom validators and extra abstractions aren’t needed. Although we all know the DRY principle, sometimes it’s acceptable to just dublicate simple validation logicdirectly into your serializer&lt;/li&gt;
&lt;/ul&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;python&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; MyModelSerializer&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;serializers&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;ModelSerializer&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;):&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    class&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt; Meta&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;        model &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; MyModel&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;        fields &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&apos;email&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &apos;username&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;        validators &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; [&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;            UniqueTogetherValidator&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;                queryset&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;MyModel.objects.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;all&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;(),&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;                fields&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&apos;email&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&apos;username&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;],&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;                message&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;This email and username combination already exists.&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;            )&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;        ]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;h3 id=&quot;summary&quot;&gt;Summary:&lt;a href=&quot;#summary&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Summary:&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Issue&lt;/strong&gt;: DRF doesn’t carry forward any custom validation messages defined in Django.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reason&lt;/strong&gt;: For reasons of architecture and backward compatibility, DRF validates its data independently from Django’s model validation.&lt;/p&gt;
&lt;h3 id=&quot;important-warnings&quot;&gt;Important warnings:&lt;a href=&quot;#important-warnings&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Important warnings:&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Be careful where you place validation logic. Django models can be modified via the admin interface, serializers, or directly with bulk operations (.update()).&lt;/p&gt;
&lt;p&gt;Placing the critical validation logic into &lt;code&gt;save()&lt;/code&gt; or &lt;code&gt;full_clean()&lt;/code&gt; is problematic, as those aren’t called when performing bulk updates or certain direct model manipulations. Always bear these edge cases in mind when developing your validation plans.&lt;/p&gt;
&lt;p&gt;This approach keeps your validation logic consistent, reusable, and well-documented throughout your project.&lt;/p&gt;
</content:encoded><category>django</category><category>drf</category><category>python</category></item><item><title>Boost your Django Docker images by using uv package and project manager</title><link>https://serhiisoldatov.dev/blog/django-docker-images-with-uv/</link><guid isPermaLink="true">https://serhiisoldatov.dev/blog/django-docker-images-with-uv/</guid><description>A multi-stage Dockerfile for Django that installs dependencies with uv, uses cache mounts for faster rebuilds, and works around a .venv permission issue. In my project the build went from 34.6s to 15.8s.</description><pubDate>Sun, 30 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;whats-uv&quot;&gt;What’s uv?&lt;a href=&quot;#whats-uv&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: What’s uv?&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;UV&lt;/strong&gt; is an ultra-fast Python package manager written in Rust by the &lt;a href=&quot;https://github.com/astral-sh&quot;&gt;Astral team&lt;/a&gt;.
You might already be familiar with another great product from Astral - &lt;a href=&quot;https://github.com/astral-sh/ruff&quot;&gt;ruff&lt;/a&gt; &lt;em&gt;(popular Python linter)&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;It drastically reduces dependency installation time and produces smaller, optimized Docker images—ideal for enhancing your Django project’s Docker workflow.
Let’s dive in!&lt;/p&gt;
&lt;h2 id=&quot;quick-start&quot;&gt;Quick start&lt;a href=&quot;#quick-start&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Quick start&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Follow these simple steps to get your local environment ready:&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;bash&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;uv&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; venv&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;    # 1. Create a UV Virtual Environment&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;source&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; .venv/bin/activate&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;    # 2. Activate the Virtual Environment&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;uv&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; sync&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; --all-groups&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;    # 3. Install Project Dependencies&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;docker-compose&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; up&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; --build&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This installs both &lt;strong&gt;production&lt;/strong&gt; and &lt;strong&gt;development&lt;/strong&gt; dependencies.
Use &lt;code&gt;uv sync --no-dev&lt;/code&gt; if you want &lt;strong&gt;production-only&lt;/strong&gt; packages.&lt;/p&gt;
&lt;h2 id=&quot;dockerfile&quot;&gt;Dockerfile&lt;a href=&quot;#dockerfile&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Dockerfile&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Here’s a breakdown of a Dockerfile designed specifically for Django applications using UV to manage Python dependencies:&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;dockerfile&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;dockerfile&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; ghcr.io/astral-sh/uv:python3.13-bookworm-slim &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;AS&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; base&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; base &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;AS&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; builder&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# Set up environment&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;ENV&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; PYTHONDONTWRITEBYTECODE=1 \&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    PYTHONUNBUFFERED=1 \&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    UV_COMPILE_BYTECODE=1 \&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    UV_LINK_MODE=copy&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;WORKDIR&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; /app&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# Install the project&apos;s dependencies using the lockfile and settings&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;RUN&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; --mount=type=cache,target=/root/.cache/uv \&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    --mount=type=bind,source=uv.lock,target=uv.lock \&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    --mount=type=bind,source=pyproject.toml,target=pyproject.toml \&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    uv sync --frozen --no-install-project --all-groups&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# Then, add the rest of the project source code and install it&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# Installing separately from its dependencies allows optimal layer caching&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;COPY&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; . /app&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;RUN&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; --mount=type=cache,target=/root/.cache/uv \&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    uv sync --frozen --all-groups&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; base&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;COPY&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; --from=builder /app /app&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;ENV&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; PATH=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;/app/.venv/bin:$PATH&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;EXPOSE&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; 8000&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;CMD&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;uv&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;run&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;python&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;manage.py&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;runserver&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;0.0.0.0:8000&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;h2 id=&quot;base-image&quot;&gt;Base image&lt;a href=&quot;#base-image&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Base image&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;We start by leveraging a slim Python image &lt;strong&gt;with UV pre-installed&lt;/strong&gt;, optimized for minimal size and maximum speed.&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;dockerfile&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;dockerfile&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; ghcr.io/astral-sh/uv:python3.13-bookworm-slim &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;AS&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; base&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; base &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;AS&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; builder&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;h2 id=&quot;environment-setup&quot;&gt;Environment setup&lt;a href=&quot;#environment-setup&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Environment setup&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;PYTHONDONTWRITEBYTECODE&lt;/code&gt; prevents Python from writing .pyc files to disk.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PYTHONUNBUFFERED&lt;/code&gt; ensures immediate logging output.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;UV_COMPILE_BYTECODE&lt;/code&gt; compiles Python bytecode for faster startup.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;UV_LINK_MODE=copy&lt;/code&gt; configures UV to copy dependencies instead of symlinking (improving compatibility).&lt;/p&gt;
&lt;p&gt;&lt;code&gt;UV_PROJECT_ENVIRONMENT&lt;/code&gt; sets the virtual environment path.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;!Permission denied issue&lt;/strong&gt;:
You might encounter permission issues when using the default virtual environment paths provided by UV as and I faced:&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Terminal output of uv sync –all-groups failing with a permission denied error when removing the .venv directory&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-1.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;You can handle this issue in two ways:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;(Recommended)&lt;/em&gt;
By adding second volume entry(&lt;code&gt;/app/.venv&lt;/code&gt;) in your docker-compose file.
Since the &lt;code&gt;.venv&lt;/code&gt; directory in the container is now completely isolated from your host filesystem and Docker can’t change ownership or permissions on your local &lt;code&gt;.venv&lt;/code&gt; folder, which solves your permission issues.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;By explicitly setting &lt;code&gt;UV_PROJECT_ENVIRONMENT&lt;/code&gt; to a dedicated path (/app/venv), we create a clean separation from default or locally-created environments, effectively resolving permission conflicts.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;dependency-installation-optimized-caching&quot;&gt;Dependency installation (optimized caching)&lt;a href=&quot;#dependency-installation-optimized-caching&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Dependency installation (optimized caching)&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;This example is adapted from the official UV example: &lt;a href=&quot;https://github.com/astral-sh/uv-docker-example/blob/main/Dockerfile&quot;&gt;Dockerfile&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;dockerfile&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;dockerfile&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;RUN&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; --mount=type=cache,target=/root/.cache/uv \&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    --mount=type=bind,source=uv.lock,target=uv.lock \&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    --mount=type=bind,source=pyproject.toml,target=pyproject.toml \&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    uv sync --frozen --no-install-project --all-groups&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;UV installs dependencies from &lt;code&gt;pyproject.toml&lt;/code&gt; and &lt;code&gt;uv.lock&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Important:&lt;/strong&gt;
Here, I’ve used &lt;code&gt;--all-groups&lt;/code&gt; to install every dependency group defined in &lt;code&gt;pyproject.toml&lt;/code&gt; (including development dependencies like linting tools). For production builds, consider switching to &lt;code&gt;--no-dev&lt;/code&gt; to install only production dependencies. For detailed guidance on managing dependency groups, refer to the official Dependency &lt;a href=&quot;https://docs.astral.sh/uv/concepts/projects/dependencies/#dependency-groups&quot;&gt;Groups documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;adding-project-source-and-finalizing-installation&quot;&gt;Adding project source and finalizing installation&lt;a href=&quot;#adding-project-source-and-finalizing-installation&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Adding project source and finalizing installation&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;dockerfile&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;dockerfile&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;COPY&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; . /app&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;RUN&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; --mount=type=cache,target=/root/.cache/uv \&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    uv sync --frozen --all-groups&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;!Note:&lt;/strong&gt; I’ve also used &lt;code&gt;--all-groups&lt;/code&gt; in there.
By adding your project’s source after installing dependencies, Docker efficiently caches layers, speeding up rebuilds when code changes occur.&lt;/p&gt;
&lt;h2 id=&quot;final-runtime-stage&quot;&gt;Final runtime stage&lt;a href=&quot;#final-runtime-stage&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: Final runtime stage&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;dockerfile&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;dockerfile&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; base&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;COPY&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; --from=builder /app /app&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;ENV&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; PATH=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;/app/venv/bin:$PATH&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;EXPOSE&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; 8000&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;CMD&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;uv&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;run&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;python&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;manage.py&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;runserver&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;0.0.0.0:8000&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;In this final image:
✅ Only the necessary files and pre-installed dependencies are copied from the builder stage.
✅ Django runs within UV’s isolated virtual environment.&lt;/p&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Important Considerations:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Production Use&lt;/strong&gt;:
This setup uses Django’s built-in development server (&lt;code&gt;runserver&lt;/code&gt;). For production environments, consider switching to a production-grade server like &lt;code&gt;gunicorn&lt;/code&gt; or &lt;code&gt;uvicorn&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Running Commands with UV&lt;/strong&gt;:
After configuring your virtual environment with UV, you &lt;strong&gt;MUST&lt;/strong&gt; prefix your commands with &lt;code&gt;uv run&lt;/code&gt;, e.g.:&lt;/li&gt;
&lt;/ul&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;bash&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;uv&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; run&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; python&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; manage.py&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; ...&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;uv&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; run&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; pytest&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; -s&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; -v&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;Otherwise, commands executed outside UV’s context will fail.&lt;/p&gt;
&lt;p&gt;Here’s how can look like your &lt;code&gt;start.sh&lt;/code&gt;:&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;bash&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;#!/usr/bin/env bash&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;set&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; -o&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; errexit&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;set&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; -o&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; pipefail&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;set&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; -o&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; nounset&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;set&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; -o&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; xtrace&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;uv&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; run&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; wait_for_postgres.py&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;uv&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; run&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; manage.py&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; migrate&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;uv&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; run&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; manage.py&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; runserver&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string)&quot;&gt; 0.0.0.0:8000&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;Your optimized Dockerfile is now ready, resulting in faster builds, smaller images, and better caching strategies for your Django application.&lt;/p&gt;
&lt;p&gt;Here’s quick time comparison:&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Build output for the new uv Dockerfile: 15.8s total, 6.7s for the dependency install step&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-2.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;quot;alt&amp;quot;:&amp;quot;Build output for the old pip-based Dockerfile: 34.6s total, 28.3s for pip install&amp;quot;,&amp;quot;src&amp;quot;:&amp;quot;./image-3.png&amp;quot;,&amp;quot;index&amp;quot;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;With the optimized Dockerfile, the build process completed in just 15.8s, with only 6.7s dedicated to installing dependencies.
In contrast, the previous Dockerfile took 34.6s, spending 28.3s installing dependencies.&lt;/p&gt;
&lt;p&gt;You can easily measure this improvement yourself by running:&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;bash&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;time&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; docker build -t container-name &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-function)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; --no-cache&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;And finally, here’s an example of &lt;code&gt;pyproject.toml&lt;/code&gt; and &lt;code&gt;docker-compose.yml&lt;/code&gt; part:&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;toml&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;toml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;[project]&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;name &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;piedpiper-web&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;version &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;0.1.0&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;description &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;Piedpiper web&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;readme &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;README.md&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;requires-python &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; &quot;&amp;gt;=3.13&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# Core application dependencies&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;dependencies &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; [&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;boto3~=1.37&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;dj-database-url==2.3.0&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;dj-rest-auth==7.0.1&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;django==5.1.7&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;django-allauth==65.3.0&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;django-autoslug==1.9.9&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;django-configurations==2.5.1&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;django-cors-headers==4.7.0&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;django-filter==25.1&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;django-model-utils==5.0.0&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;django-role-permissions==3.2.0&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;django-storages==1.14.5&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;django-unique-upload==0.2.1&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;djangorestframework==3.15.2&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;djangorestframework-api-key==3.0.0&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;djangorestframework-simplejwt==5.5.0&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;gunicorn==23.0.0&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;pillow~=11.1&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;psycopg2-binary==2.9.10&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;python-dotenv==1.0.1&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;requests==2.32.3&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;setuptools==77.0.3&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;[dependency-groups]&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# Linting and code quality dependencies&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;lint &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; [&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;black==25.1.0&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;flake8==7.1.2&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;isort==6.0.1&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;pre-commit==4.2.0&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;ruff==0.11.2&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-comment)&quot;&gt;# dev dependencies&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;dev &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt; [&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;django-silk&amp;gt;=5.3.2&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;nplusone&amp;gt;=1.0.0&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;ipdb==0.13.13&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;ipython==8.34.0&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;mock==5.2.0&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;coverage~=7.7&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;pytest-django==4.10.0&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;    &quot;factory-boy==3.3.3&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;    &quot;drf-yasg~=1.21&quot;,&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;&lt;code&gt;docker-compose&lt;/code&gt;:&lt;/p&gt;
&lt;figure class=&quot;sr-code&quot;&gt;&lt;figcaption class=&quot;sr-code__bar&quot;&gt;&lt;span class=&quot;sr-meta&quot;&gt;yaml&lt;/span&gt;&lt;button class=&quot;sr-code__copy sr-meta&quot; type=&quot;button&quot;&gt;Copy&lt;/button&gt;&lt;/figcaption&gt;&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;  web&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    restart&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; always&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    build&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;      context&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-constant)&quot;&gt; .&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;      dockerfile&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; Dockerfile&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;      target&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; builder&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    command&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; bash scripts/start.sh&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    working_dir&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt; /app&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    volumes&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;./:/app&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;/app/.venv&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    ports&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;&quot;8000:8000&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    depends_on&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;postgres&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;    env_file&lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;line ln&quot;&gt;&lt;span style=&quot;color:var(--astro-code-foreground)&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:var(--astro-code-token-string-expression)&quot;&gt;.env&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;h2 id=&quot;more-resources&quot;&gt;More resources&lt;a href=&quot;#more-resources&quot; class=&quot;heading-anchor&quot; aria-label=&quot;Link to section: More resources&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astral.sh/uv/&quot;&gt;Official documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.saaspegasus.com/guides/uv-deep-dive/&quot;&gt;uv: An In-Depth Guide to Python’s Fast and Ambitious New Package Manager&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://depot.dev/docs/container-builds/how-to-guides/optimal-dockerfiles/python-uv-dockerfile&quot;&gt;Best practice Dockerfile for Python with uv&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Hope it was useful,
Thanks for reading 🙌&lt;/p&gt;
</content:encoded><category>python</category><category>uv</category><category>django</category><category>docker</category></item></channel></rss>