<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Djangix]]></title><description><![CDATA[Djangix]]></description><link>https://djangix.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>Djangix</title><link>https://djangix.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sat, 10 Oct 2026 11:56:07 GMT</lastBuildDate><atom:link href="https://djangix.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[JWT Authentication in Django: Fix 'Invalid Token' and Token Expired 401 Errors]]></title><description><![CDATA[Originally published on djangix.com
JWT authentication in Django usually breaks in a very specific way: login succeeds, you copy the access token, and the very next request comes back with 401 Unautho]]></description><link>https://djangix.hashnode.dev/jwt-authentication-in-django-fix-invalid-token-and-token-expired-401-errors</link><guid isPermaLink="true">https://djangix.hashnode.dev/jwt-authentication-in-django-fix-invalid-token-and-token-expired-401-errors</guid><category><![CDATA[Django]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[Ubaid Ullah]]></dc:creator><pubDate>Sat, 10 Oct 2026 08:21:17 GMT</pubDate><content:encoded><![CDATA[<p><em>Originally published on <a href="https://djangix.com/blog/jwt-authentication-django-invalid-token-expired/">djangix.com</a></em></p>
<p>JWT authentication in Django usually breaks in a very specific way: login succeeds, you copy the access token, and the very next request comes back with 401 Unauthorized. The response body is the whole diagnosis — if you read it before changing code, this is a ten-minute fix instead of an afternoon of guessing.</p>
<p><img src="https://djangix.com/media/eb1e3858-39e3-4dee-b339-3deae6a44138.jpg" alt="Developer looking at an API rate limit error notification on a monitor" /></p>
<p>The short answer is this: a Simple JWT 401 almost always means one of four things. The access token has expired, the wrong token (a refresh token) was sent to an API endpoint, the Authorization header never reached Django, or the token was signed with a different secret than the server now uses. Below is how to tell which one you have, and the correct fix for each.</p>
<h2>What the JWT authentication error actually says</h2>
<p>Django REST Framework with Simple JWT returns a JSON body, not just a status code. Read the <code>detail</code>, <code>code</code>, and the per-message fields:</p>
<pre><code class="language-json">{
  "detail": "Given token not valid for any token type",
  "code": "token_not_valid",
  "messages": [
    {
      "token_class": "AccessToken",
      "token_type": "access",
      "message": "Token is invalid or expired"
    }
  ]
}
</code></pre>
<p>That one body already narrows the problem down:</p>
<ul>
<li><strong>code token_not_valid, message Token is invalid or expired:</strong> the token failed validation — it is expired, malformed, blacklisted, or signed with a different key. Check the steps below in order.</li>
<li><strong>detail Authentication credentials were not provided:</strong> this is a different bug. No usable Authorization header reached the view at all, so Django never even saw a token. Skip to the header check below.</li>
<li><strong>detail Given token not valid for any token type, with a valid-looking token:</strong> you very often sent the refresh token where an access token belongs. The two token types are not interchangeable.</li>
<li><strong>Token is blacklisted:</strong> the refresh token was already rotated or logged out. The client must log in again — no server-side setting will resurrect that token.</li>
</ul>
<h2>JWT authentication settings that cause most 401 errors</h2>
<p>Most production 401s are configuration, not code. This is a known-good baseline for Simple JWT 5.5.1, the current release on PyPI at the time of writing:</p>
<pre><code class="language-python"># settings.py
from datetime import timedelta

INSTALLED_APPS = [
    # ...
    "rest_framework",
    "rest_framework_simplejwt.token_blacklist",
]

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": (
        "rest_framework_simplejwt.authentication.JWTAuthentication",
    ),
    "DEFAULT_PERMISSION_CLASSES": (
        "rest_framework.permissions.IsAuthenticated",
    ),
}

SIMPLE_JWT = {
    "ACCESS_TOKEN_LIFETIME": timedelta(minutes=15),
    "REFRESH_TOKEN_LIFETIME": timedelta(days=7),
    "ROTATE_REFRESH_TOKENS": True,
    "BLACKLIST_AFTER_ROTATION": True,
    "AUTH_HEADER_TYPES": ("Bearer",),
}
</code></pre>
<p>Three settings in that block explain most surprises:</p>
<ul>
<li><strong>Short default access lifetime:</strong> out of the box, Simple JWT access tokens live for five minutes and refresh tokens for one day. If your frontend never calls the refresh endpoint, every user appears to be logged out after five minutes. That is expiry working as designed, not a bug.</li>
<li><strong>Signing key follows SECRET_KEY:</strong> by default tokens are signed with your Django SECRET_KEY. Regenerate or change SECRET_KEY in production and every existing token instantly becomes invalid. Users must log in again, and no code fix changes that.</li>
<li><strong>The blacklist app must be installed:</strong> if you enable blacklist-after-rotation without adding the token_blacklist app and running migrations, rotation breaks. Install the app, migrate, then enable the setting.</li>
</ul>
<p>Wire the endpoints once, in your root URL configuration:</p>
<pre><code class="language-python"># urls.py
from django.urls import path
from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
    TokenVerifyView,
)

urlpatterns = [
    path("api/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
    path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
    path("api/token/verify/", TokenVerifyView.as_view(), name="token_verify"),
]
</code></pre>
<h2>Fix JWT token expired errors with a real refresh flow</h2>
<p>Expired access tokens are normal. The bug is a client that treats expiry as a logout instead of refreshing once and retrying the original request:</p>
<pre><code class="language-javascript">async function apiFetch(url, options = {}) {
  const makeRequest = (access) =&gt; fetch(url, {
    ...options,
    headers: {
      ...(options.headers || {}),
      Authorization: `Bearer ${access}`,
    },
  });

  let response = await makeRequest(localStorage.getItem("access"));
  if (response.status !== 401) return response;

  const refreshResponse = await fetch("/api/token/refresh/", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ refresh: localStorage.getItem("refresh") }),
  });
  if (!refreshResponse.ok) return response; // refresh is dead: log the user out

  const data = await refreshResponse.json();
  localStorage.setItem("access", data.access);
  if (data.refresh) localStorage.setItem("refresh", data.refresh);
  return makeRequest(data.access);
}
</code></pre>
<p>Note the two details people miss. First, with rotation enabled the refresh response can contain a new refresh token — store it, or the next refresh will present a blacklisted token. Second, only retry on 401, and only once. Retrying in a loop turns a dead session into a request storm.</p>
<p>Still not sure whether the token itself is the problem? Inspect it server-side before blaming the frontend:</p>
<pre><code class="language-python">from rest_framework_simplejwt.tokens import AccessToken

token = AccessToken("paste-the-access-token-here")
print(token["token_type"])  # must be "access"
print(token["user_id"])
print(token["exp"])         # UTC epoch seconds: compare with your server clock
</code></pre>
<p>If constructing the token raises an error, the token is malformed or signed with another key. If it parses but the exp value is in the past, it is simply expired and the refresh flow above is the fix. Also check server time: a container or VM with a skewed clock will reject fresh tokens as not yet valid or already expired.</p>
<p>One related trap: a browser can drop the Authorization header before Django ever sees it. If your API and frontend are on different origins and preflight requests fail, treat it as a CORS problem first — see our guide to being <a href="https://djangix.com/blog/blocked-by-cors-policy/">blocked by the CORS policy</a>. And do not confuse this with cookie-session failures: header-based JWT auth does not need a CSRF token, which is a separate mechanism covered in our post on <a href="https://djangix.com/blog/django-csrf-verification-failed/">Django CSRF verification failed</a>. Verifying a credential before trusting it is the same discipline we apply to webhooks in our <a href="https://djangix.com/blog/stripe-webhook-verification-django/">Stripe webhook verification guide</a>.</p>
<h2>The wrong fixes for invalid token errors</h2>
<p>These appear in almost every forum thread, and each one trades a small bug for a bigger one:</p>
<ul>
<li><strong>Stretching the access token lifetime to 30 days:</strong> the 401s stop, but a stolen access token now works for a month and cannot be revoked. Keep access tokens short and fix the refresh flow instead.</li>
<li><strong>Turning off expiry checks or adding a huge leeway:</strong> a large LEEWAY setting hides clock problems for a while and quietly accepts tokens that should be dead.</li>
<li><strong>Commenting out IsAuthenticated to make the error go away:</strong> the endpoint now works for everyone, including people with no token at all. You have removed the lock, not fixed the key.</li>
<li><strong>Regenerating SECRET_KEY every deploy:</strong> every deploy logs out every user and invalidates every token, because the signing key changed. Keep SECRET_KEY stable in your environment, and rotate it only as a deliberate, communicated event.</li>
<li><strong>Sending whichever token is at hand:</strong> access and refresh tokens have different token_type claims and different jobs. An endpoint that expects an access token will correctly reject a refresh token every time.</li>
</ul>
<h2>Final checklist for JWT authentication in Django</h2>
<p>Work through these in order and the 401 will name itself:</p>
<ul>
<li><strong>Read the response body first:</strong> token_not_valid, credentials not provided, and blacklisted tokens are three different bugs with three different fixes.</li>
<li><strong>Confirm the token type:</strong> decode the token and check that token_type is access for API calls.</li>
<li><strong>Check expiry and server time:</strong> compare the exp claim with a correct UTC clock on the server.</li>
<li><strong>Verify the header arrives:</strong> confirm the Authorization Bearer header reaches Django, through your proxy and CORS configuration.</li>
<li><strong>Keep SECRET_KEY stable:</strong> if it changed, old tokens are invalid by design and users must log in again.</li>
<li><strong>Implement refresh properly:</strong> refresh once on 401, store any rotated refresh token, retry the original request once, and log out cleanly if the refresh token is rejected.</li>
</ul>
<p>Do that, and JWT authentication stops being a mystery error and becomes what it should be: a short-lived access token, a working refresh flow, and a 401 you can explain in one sentence.</p>
]]></content:encoded></item><item><title><![CDATA[SaaS Product Development: From Idea to Launch, Step by Step]]></title><description><![CDATA[Originally published on the Djangix blog. This is a condensed version — the full article is linked at the end.
Building a SaaS product is less a single build than a sequence of decisions. Doing them i]]></description><link>https://djangix.hashnode.dev/saas-product-development-from-idea-to-launch-step-by-step</link><guid isPermaLink="true">https://djangix.hashnode.dev/saas-product-development-from-idea-to-launch-step-by-step</guid><category><![CDATA[SaaS]]></category><dc:creator><![CDATA[Ubaid Ullah]]></dc:creator><pubDate>Fri, 09 Oct 2026 06:59:14 GMT</pubDate><content:encoded><![CDATA[<p><em>Originally published on the <a href="https://djangix.com/blog/saas-product-development/">Djangix blog</a>. This is a condensed version — the full article is linked at the end.</em></p>
<p>Building a SaaS product is less a single build than a sequence of decisions. Doing them in the wrong order is what makes projects expensive, according to the original article.</p>
<h2>The sequence, summarised</h2>
<ol>
<li><strong>Validate the problem first.</strong> Talk to the people who would pay, and confirm the pain is frequent and costly enough to justify software.</li>
<li><strong>Define the smallest useful version.</strong> Separate the core workflow that must work on day one from everything that can wait.</li>
<li><strong>Design around the workflow, not features.</strong> Map the main user journey end to end before screens multiply.</li>
<li><strong>Choose boring, proven foundations.</strong> Accounts, payments and data separation are not places to experiment.</li>
<li><strong>Build in visible slices.</strong> Working software shown regularly beats a long silent build with a big reveal.</li>
<li><strong>Prepare for launch as its own step.</strong> Billing checks, backups, monitoring and a support path need attention before users arrive.</li>
<li><strong>Plan the post-launch loop.</strong> Early feedback, fixes and small improvements are part of development, not a separate phase.</li>
</ol>
<p>Each stage reduces the risk of the next, and skipping early stages simply moves their cost later, with interest.</p>
<hr />
<p><strong>Read the full article on Djangix:</strong> <a href="https://djangix.com/blog/saas-product-development/">SaaS Product Development: From Idea to Launch, Step by Step</a> — with the complete step-by-step process.</p>
]]></content:encoded></item><item><title><![CDATA[Stripe API Integration for SaaS: Connect Payments Without the Usual Mistakes]]></title><description><![CDATA[Originally published on the Djangix blog. This is a condensed version — the full article is linked at the end.
Adding payments to a SaaS product looks simple until the first edge case: a card fails af]]></description><link>https://djangix.hashnode.dev/stripe-api-integration-for-saas-connect-payments-without-the-usual-mistakes</link><guid isPermaLink="true">https://djangix.hashnode.dev/stripe-api-integration-for-saas-connect-payments-without-the-usual-mistakes</guid><category><![CDATA[stripe]]></category><dc:creator><![CDATA[Ubaid Ullah]]></dc:creator><pubDate>Fri, 09 Oct 2026 06:56:35 GMT</pubDate><content:encoded><![CDATA[<p><em>Originally published on the <a href="https://djangix.com/blog/stripe-api-integration/">Djangix blog</a>. This is a condensed version — the full article is linked at the end.</em></p>
<p>Adding payments to a SaaS product looks simple until the first edge case: a card fails after access was granted, a customer is charged twice, or a cancellation never reaches your database. The original article treats billing as a state problem, not just a checkout form.</p>
<h2>The core ideas, summarised</h2>
<ul>
<li><strong>Model billing in your own data first.</strong> Keep customer and subscription identifiers and current status on your side, so your product does not depend on a live call to decide who has access.</li>
<li><strong>Use a hosted checkout for the first version.</strong> It handles card collection and common payment methods far better than a custom form built quickly.</li>
<li><strong>Let verified server-side events drive access.</strong> Payment and subscription changes should update your records from notifications arriving at your server — not from what the browser reports after redirect.</li>
<li><strong>Handle the awkward lifecycle events.</strong> Failed renewals, cancellations, plan changes and refunds each need an explicit response in your access logic.</li>
<li><strong>Make retries safe.</strong> Repeating a create call after a timeout should not create a second charge — use idempotency and test that path.</li>
<li><strong>Test with simulated events before going live</strong>, including failure cases.</li>
</ul>
<p>Most billing bugs are lifecycle bugs, not checkout bugs.</p>
<hr />
<p><strong>Read the full article on Djangix:</strong> <a href="https://djangix.com/blog/stripe-api-integration/">Stripe API Integration for SaaS</a> — with the full architecture, event list and mistakes checklist.</p>
]]></content:encoded></item><item><title><![CDATA[select_related vs prefetch_related: The Django N+1 Fix, Explained With Query Counts]]></title><description><![CDATA[Originally published on the Djangix blog. This is a condensed version — the full article is linked at the end.
A Django page that feels instant with a handful of rows can crawl once the table grows. O]]></description><link>https://djangix.hashnode.dev/select-related-vs-prefetch-related-the-django-n-1-fix-explained-with-query-counts</link><guid isPermaLink="true">https://djangix.hashnode.dev/select-related-vs-prefetch-related-the-django-n-1-fix-explained-with-query-counts</guid><category><![CDATA[Django]]></category><dc:creator><![CDATA[Ubaid Ullah]]></dc:creator><pubDate>Fri, 09 Oct 2026 06:54:48 GMT</pubDate><content:encoded><![CDATA[<p><em>Originally published on the <a href="https://djangix.com/blog/django-select-related-vs-prefetch-related/">Djangix blog</a>. This is a condensed version — the full article is linked at the end.</em></p>
<p>A Django page that feels instant with a handful of rows can crawl once the table grows. Often the cause is not the database itself, but how many separate trips the ORM makes to it.</p>
<h2>The N+1 pattern, in plain terms</h2>
<p>Fetch a list in one query, then follow a related object for each row — and each follow-up is another query. Ten rows hide the problem; a thousand rows turn it into a thousand extra trips.</p>
<h2>Choosing the right tool</h2>
<ul>
<li><strong>For a single-valued relationship</strong> — such as the one author of an article — fetch the related row together with the main row in a joined query. One trip, and later access is free.</li>
<li><strong>For a collection</strong> — such as all the comments belonging to each item — fetch the related rows in a separate batched query and match them in Python. Joining collections into the main query can multiply rows and repeat parent data.</li>
</ul>
<p>That direction — single-valued versus collection — is the core decision rule in the original article.</p>
<h2>Habits that keep you honest</h2>
<p>Count queries in tests and development rather than guessing, watch what templates and serializers actually access, and remember the limits: these helpers do not remove queries for later separate querysets.</p>
<p>The short version: measure the query count, match the helper to the shape of the relationship, and re-check after you change it.</p>
<hr />
<p><strong>Read the full article on Djangix:</strong> <a href="https://djangix.com/blog/django-select-related-vs-prefetch-related/">select_related vs prefetch_related: The Django N+1 Fix</a> — with worked examples and the query count at each step.</p>
]]></content:encoded></item><item><title><![CDATA[Content Security Policy Blocking Your Scripts? Fix "Refused to Execute Inline Script" Properly]]></title><description><![CDATA[Originally published on the Djangix blog. This is a condensed version — the full article is linked at the end.
You add a Content Security Policy, deploy, and the site looks fine but nothing responds. ]]></description><link>https://djangix.hashnode.dev/content-security-policy-blocking-your-scripts-fix-refused-to-execute-inline-script-properly</link><guid isPermaLink="true">https://djangix.hashnode.dev/content-security-policy-blocking-your-scripts-fix-refused-to-execute-inline-script-properly</guid><category><![CDATA[JavaScript]]></category><dc:creator><![CDATA[Ubaid Ullah]]></dc:creator><pubDate>Fri, 09 Oct 2026 06:54:11 GMT</pubDate><content:encoded><![CDATA[<p><em>Originally published on the <a href="https://djangix.com/blog/content-security-policy-errors/">Djangix blog</a>. This is a condensed version — the full article is linked at the end.</em></p>
<p>You add a Content Security Policy, deploy, and the site looks fine but nothing responds. That is not the policy failing — it is the policy working. Your own inline code simply is not allowed yet.</p>
<h2>What the policy is doing</h2>
<p>CSP is a browser-enforced allowlist for scripts, styles, images and connections. Its main security value is against injected scripts, and injected code is usually inline — so a strict policy blocks inline code by default, including yours.</p>
<h2>Five fixes, in the order to try them</h2>
<ol>
<li><strong>Move inline code to external files.</strong> A script file served from your own origin is already allowed by a self-only script rule, and it also gains caching and linting.</li>
<li><strong>Use a fresh nonce for code that must stay inline.</strong> Generate a random value for every response, put the same value in the policy and the tag, and never reuse it.</li>
<li><strong>Hash a script that never changes.</strong> A fixed snippet can be allowed by its hash — but any edit, even formatting, changes the hash and blocks it again.</li>
<li><strong>Replace inline handlers with listeners.</strong> Inline click handlers in markup are blocked, which is why pages can load while buttons do nothing. Move the behaviour into JavaScript and attach listeners there.</li>
<li><strong>Name third parties explicitly.</strong> Analytics, chat and embeds each need their own origins listed in the relevant directives. Do not use a wildcard — that also allows an attacker's origin.</li>
</ol>
<h2>The shortcut to avoid</h2>
<p>Allowing inline scripts generally makes the errors disappear by disabling the protection CSP exists to provide. If that is the fix, the policy is mostly decoration.</p>
<h2>Safer rollout</h2>
<p>Start in report-only mode: the browser enforces nothing but reports violations, so you can find and fix real usage before enforcing the same policy for users.</p>
<hr />
<p><strong>Read the full article on Djangix:</strong> <a href="https://djangix.com/blog/content-security-policy-errors/">Content Security Policy Blocking Your Scripts?</a> — including the Django nonce middleware example and the full ranked fixes.</p>
]]></content:encoded></item><item><title><![CDATA[Streaming OpenAI API Responses: How It Works, in Python, Django & FastAPI]]></title><description><![CDATA[Originally published on the Djangix blog. This is a condensed version — the full article, with complete code, is linked at the end.
A non-streamed chat answer leaves the user staring at a spinner for ]]></description><link>https://djangix.hashnode.dev/streaming-openai-api-responses-how-it-works-in-python-django-fastapi</link><guid isPermaLink="true">https://djangix.hashnode.dev/streaming-openai-api-responses-how-it-works-in-python-django-fastapi</guid><category><![CDATA[Python]]></category><dc:creator><![CDATA[Ubaid Ullah]]></dc:creator><pubDate>Fri, 09 Oct 2026 06:53:19 GMT</pubDate><content:encoded><![CDATA[<p><em>Originally published on the <a href="https://djangix.com/blog/streaming-openai-api-responses/">Djangix blog</a>. This is a condensed version — the full article, with complete code, is linked at the end.</em></p>
<p>A non-streamed chat answer leaves the user staring at a spinner for the whole generation. Streaming changes the feel, not the total time: the first tokens arrive in under a second and the answer appears to type itself.</p>
<h2>What is actually sent</h2>
<p>The transport is Server-Sent Events — one HTTP response that stays open and delivers small messages. Each message carries a JSON chunk, and the useful text lives in a tiny delta field. Most chunks are fragments, not whole words, so the client simply concatenates them until a final done marker arrives.</p>
<h2>Going through your own backend</h2>
<p>Browsers should not call the provider directly, because that would expose your API key. So your server receives the chunks and re-emits them — and that middle step is where many implementations break.</p>
<ul>
<li><strong>Wrap each token as JSON.</strong> Raw token text can contain newlines, which break the event framing. Encoding the token inside a JSON object avoids mangled multi-line answers.</li>
<li><strong>Read with fetch, not EventSource, for POST chats.</strong> The built-in event API cannot send a JSON body or an authorization header, so read the response stream manually and keep a buffer for events split across network chunks.</li>
<li><strong>Ask for usage explicitly.</strong> A stream option is needed if you want a final usage chunk for billing or budgeting.</li>
</ul>
<h2>Production pitfalls worth planning for</h2>
<ul>
<li><strong>Buffering proxies:</strong> Reverse proxies and CDNs may hold the response and deliver it all at once, silently defeating streaming. Disable buffering for that route and test through the real production path.</li>
<li><strong>Errors after the stream starts:</strong> Once the first chunk is sent, you cannot change the status code. Send an error as an event instead and render it in the UI.</li>
<li><strong>Disconnected users and saving:</strong> Stop consuming when the client goes away, and save the assembled answer once at the end rather than writing to the database per token.</li>
<li><strong>Idle timeouts:</strong> Periodic keepalive comments can keep a long generation from being cut off.</li>
</ul>
<h2>When not to stream</h2>
<p>Short answers gain little, machine-consumed structured output usually needs to be complete before it can be parsed, and background jobs have no one watching. Streaming is a user-experience feature — use it where a person is waiting.</p>
<hr />
<p><strong>Read the full article on Djangix:</strong> <a href="https://djangix.com/blog/streaming-openai-api-responses/">Streaming OpenAI API Responses: How It Works, in Python, Django &amp; FastAPI</a> — with the complete Python, Django and FastAPI code.</p>
]]></content:encoded></item><item><title><![CDATA[Building SafetyGraph: 546,891 Industrial Incident Records in One Queryable Dataset]]></title><description><![CDATA[Originally published on the Djangix blog. This is a condensed version — read the full article on djangix.com at the link below.
US industrial safety data is public, but it has never been convenient: s]]></description><link>https://djangix.hashnode.dev/building-safetygraph-546-891-industrial-incident-records-in-one-queryable-dataset</link><guid isPermaLink="true">https://djangix.hashnode.dev/building-safetygraph-546-891-industrial-incident-records-in-one-queryable-dataset</guid><category><![CDATA[Django]]></category><category><![CDATA[Python]]></category><category><![CDATA[data]]></category><dc:creator><![CDATA[Ubaid Ullah]]></dc:creator><pubDate>Fri, 09 Oct 2026 06:49:31 GMT</pubDate><content:encoded><![CDATA[<p><em>Originally published on the <a href="https://djangix.com/blog/building-safetygraph-546-891-industrial-incident-records-in-one-queryable-datase/">Djangix blog</a>. This is a condensed version — read the full article on djangix.com at the link below.</em></p>
<p>US industrial safety data is public, but it has never been convenient: separate agency systems, separate search forms, and records that sit in isolation from each other. SafetyGraph, described in the full article, unifies them into one dataset holding more than half a million incident records.</p>
<h2>The hard part is identity</h2>
<p>The same employer appears under name variants, punctuation differences, and renames, so the pipeline normalises names and matches conservatively, erring toward separate records when a link is uncertain. Establishments, inspections, violations, and accidents are then related explicitly instead of being left as disconnected rows.</p>
<h2>What one queryable dataset enables</h2>
<p>With those links, a company's history across sources, or the chain from an inspection to its findings and outcome, becomes a single query rather than hours of manual cross-checking — the form safety teams, researchers, journalists, and data builders actually need.</p>
<h2>Read the full article</h2>
<p>This was a condensed summary. The complete story — sources, matching, data model, and example questions — is on the Djangix blog: <a href="https://djangix.com/blog/building-safetygraph-546-891-industrial-incident-records-in-one-queryable-datase/">Building SafetyGraph: 546,891 Industrial Incident Records in One Queryable Dataset</a></p>
]]></content:encoded></item><item><title><![CDATA[How Long Does It Take to Build a SaaS MVP? An Honest Timeline]]></title><description><![CDATA[Originally published on the Djangix blog. This is a condensed version — read the full article on djangix.com at the link below.
There is no single honest number for a SaaS MVP — there is a range, and ]]></description><link>https://djangix.hashnode.dev/how-long-does-it-take-to-build-a-saas-mvp-an-honest-timeline</link><guid isPermaLink="true">https://djangix.hashnode.dev/how-long-does-it-take-to-build-a-saas-mvp-an-honest-timeline</guid><category><![CDATA[SaaS]]></category><category><![CDATA[startup]]></category><category><![CDATA[Django]]></category><dc:creator><![CDATA[Ubaid Ullah]]></dc:creator><pubDate>Fri, 09 Oct 2026 06:48:29 GMT</pubDate><content:encoded><![CDATA[<p><em>Originally published on the <a href="https://djangix.com/blog/how-long-does-it-take-to-build-a-saas-mvp/">Djangix blog</a>. This is a condensed version — read the full article on djangix.com at the link below.</em></p>
<p>There is no single honest number for a SaaS MVP — there is a range, and scope moves it far more than coding speed does. The full guide breaks that range down by product type and by phase.</p>
<h2>What you are really estimating</h2>
<p>A narrow product with one core job, accounts, and payment can reach real customers in weeks. Add many user roles, layered permissions, several third-party integrations, or regulated data and you are planning in months, for reasons that have little to do with typing.</p>
<h2>Where the time actually goes</h2>
<p>Validation and definition, a foundation of accounts and core data, the main build, then billing, hardening, and launch. In practice the delays collect between phases — decisions, designs, credentials, content, and feedback — so fast, clear decisions shorten a project as surely as fast development.</p>
<h2>Stretchers and shortcuts</h2>
<p>Mid-build additions, vague requirements, bespoke design everywhere, and unchecked integration approvals stretch timelines. A single clear outcome, proven services for auth and payments, and cutting scope instead of quality to meet a chosen launch date keep them honest.</p>
<h2>Read the full article</h2>
<p>This was a condensed summary. The complete timeline, with phase ranges and an estimation checklist, is on the Djangix blog: <a href="https://djangix.com/blog/how-long-does-it-take-to-build-a-saas-mvp/">How Long Does It Take to Build a SaaS MVP? An Honest Timeline</a></p>
]]></content:encoded></item><item><title><![CDATA[Deploying Django on AWS with Zero Downtime]]></title><description><![CDATA[Originally published on the Djangix blog. This is a condensed version — read the full article on djangix.com at the link below.
You do not need a cluster to deploy without downtime. The pattern in the]]></description><link>https://djangix.hashnode.dev/deploying-django-on-aws-with-zero-downtime</link><guid isPermaLink="true">https://djangix.hashnode.dev/deploying-django-on-aws-with-zero-downtime</guid><category><![CDATA[Django]]></category><category><![CDATA[AWS]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[Ubaid Ullah]]></dc:creator><pubDate>Fri, 09 Oct 2026 06:47:12 GMT</pubDate><content:encoded><![CDATA[<p><em>Originally published on the <a href="https://djangix.com/blog/deploying-django-aws-zero-downtime/">Djangix blog</a>. This is a condensed version — read the full article on djangix.com at the link below.</em></p>
<p>You do not need a cluster to deploy without downtime. The pattern in the full guide runs blue-green on a single EC2 host: two complete copies of the app, one live, one idle, with Nginx switching traffic between them.</p>
<h2>Two colours, one server</h2>
<p>Each colour gets its own directory, virtualenv, Gunicorn service, and port. The database, media, and environment stay shared, so switching colours changes code — not data.</p>
<h2>Deploy to the idle colour</h2>
<p>Update the inactive copy, run checks, start it, and probe its health endpoint on its own port. Only when it answers correctly do you update the Nginx upstream and reload, sending users to the new release.</p>
<h2>Rollback and the traps</h2>
<p>The old colour keeps running, so rollback is another proxy switch, not a rebuild. Guard the details the guide calls out: keep secrets out of the app folders, never let a deploy overwrite static files the live colour is serving, and treat database changes the old code cannot read as a separate, careful step.</p>
<h2>Read the full article</h2>
<p>This was a condensed summary. The complete walkthrough, with commands and configuration, is on the Djangix blog: <a href="https://djangix.com/blog/deploying-django-aws-zero-downtime/">Deploying Django on AWS with Zero Downtime</a></p>
]]></content:encoded></item><item><title><![CDATA[Blocked by CORS Policy: Every Console Message and Its Real Fix]]></title><description><![CDATA[Originally published on the Djangix blog. This is a condensed version — read the full article on djangix.com at the link below.
A CORS block is the browser enforcing a server's answer, so the fix almo]]></description><link>https://djangix.hashnode.dev/blocked-by-cors-policy-every-console-message-and-its-real-fix</link><guid isPermaLink="true">https://djangix.hashnode.dev/blocked-by-cors-policy-every-console-message-and-its-real-fix</guid><category><![CDATA[Django]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[CORS]]></category><dc:creator><![CDATA[Ubaid Ullah]]></dc:creator><pubDate>Fri, 09 Oct 2026 06:45:30 GMT</pubDate><content:encoded><![CDATA[<p><em>Originally published on the <a href="https://djangix.com/blog/blocked-by-cors-policy/">Djangix blog</a>. This is a condensed version — read the full article on djangix.com at the link below.</em></p>
<p>A CORS block is the browser enforcing a server's answer, so the fix almost never lives in the JavaScript that got blocked. The console message is a family of distinct complaints, and the clause after the headline tells you which one you have.</p>
<h2>Missing or mismatched origin</h2>
<p>If the response carries no matching Access-Control-Allow-Origin value, check the exact origin — scheme, host, and port all count — and remember that wildcard answers stop working once credentials are involved.</p>
<h2>Failed preflight</h2>
<p>Requests with custom headers or non-simple methods trigger an OPTIONS preflight first. If the server does not allow the method and headers you send, the real request is never made.</p>
<h2>Headers, credentials, and look-alikes</h2>
<p>Response headers stay hidden from JavaScript unless the server exposes them, credentialed calls need explicit permission, and some apparent CORS failures are really redirects, server errors, or mixed content wearing a CORS costume. In Django, the full guide configures the CORS middleware and its allowlists deliberately so each case is handled on the server side.</p>
<h2>Read the full article</h2>
<p>This was a condensed summary. The complete guide, message by message, is on the Djangix blog: <a href="https://djangix.com/blog/blocked-by-cors-policy/">Blocked by CORS Policy: Every Console Message and Its Real Fix</a></p>
]]></content:encoded></item><item><title><![CDATA[OpenAI API Rate Limit Errors (429): Which Ones to Retry and Which Ones to Stop]]></title><description><![CDATA[Originally published on the Djangix blog. This is a condensed version — read the full article on djangix.com at the link below.
A 429 tells you a limit was hit, but not whether waiting will help. That]]></description><link>https://djangix.hashnode.dev/openai-api-rate-limit-errors-429-which-ones-to-retry-and-which-ones-to-stop</link><guid isPermaLink="true">https://djangix.hashnode.dev/openai-api-rate-limit-errors-429-which-ones-to-retry-and-which-ones-to-stop</guid><category><![CDATA[Python]]></category><category><![CDATA[openai]]></category><category><![CDATA[api]]></category><dc:creator><![CDATA[Ubaid Ullah]]></dc:creator><pubDate>Fri, 09 Oct 2026 06:44:09 GMT</pubDate><content:encoded><![CDATA[<p><em>Originally published on the <a href="https://djangix.com/blog/openai-api-rate-limit-errors/">Djangix blog</a>. This is a condensed version — read the full article on djangix.com at the link below.</em></p>
<p>A 429 tells you a limit was hit, but not whether waiting will help. That depends on which limit it was, so start from the error code and the rate-limit headers rather than the status alone.</p>
<h2>Retryable: temporary rate pressure</h2>
<p>Short-lived requests-per-minute and tokens-per-minute limits clear on their own. For these, back off and retry — the SDK already retries a small number of times by default, and a custom policy should add exponential delay, random jitter, and an upper bound so retries do not arrive in a synchronised wave.</p>
<h2>Not retryable: quota and spend limits</h2>
<p>When the error points to exhausted quota, billing, or a project spend cap, retrying changes nothing. Stop calling, tell the user or operator plainly, and fix the underlying account issue first.</p>
<h2>Lower your request rate for good</h2>
<p>Cache repeated prompts, deduplicate identical calls, batch where the work allows it, cap max_tokens realistically, trim oversized context, and send simple jobs to a smaller model. For bursty background work, a queue that paces requests beats immediate retries from every worker.</p>
<h2>Read the full article</h2>
<p>This was a condensed summary. The complete guide, with the decision table and retry code, is on the Djangix blog: <a href="https://djangix.com/blog/openai-api-rate-limit-errors/">OpenAI API Rate Limit Errors (429): Which Ones to Retry and Which Ones to Stop</a></p>
]]></content:encoded></item><item><title><![CDATA[Fixing 'CSRF Verification Failed' in Django]]></title><description><![CDATA[Originally published on the Djangix blog. This is a condensed version — read the full article on djangix.com at the link below.
“CSRF verification failed” is Django protecting you — a POST arrived wit]]></description><link>https://djangix.hashnode.dev/fixing-csrf-verification-failed-in-django</link><guid isPermaLink="true">https://djangix.hashnode.dev/fixing-csrf-verification-failed-in-django</guid><category><![CDATA[Django]]></category><category><![CDATA[Python]]></category><category><![CDATA[debugging]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Ubaid Ullah]]></dc:creator><pubDate>Fri, 09 Oct 2026 06:33:21 GMT</pubDate><content:encoded><![CDATA[<p><em>Originally published on the <a href="https://djangix.com/blog/django-csrf-verification-failed/">Djangix blog</a>. This is a condensed version — read the full article on djangix.com at the link below.</em></p>
<p>“CSRF verification failed” is Django protecting you — a POST arrived without a valid CSRF token. Work through the causes below in order; one of them is almost always responsible.</p>
<h2>1. Missing csrf_token in the form</h2>
<p>Every POST form rendered by Django needs the CSRF token tag inside it. Without it, the browser sends no token and the middleware rejects the request with a 403. This is the first thing to check, especially on hand-written templates and forms added by JavaScript.</p>
<h2>2. CSRF_TRUSTED_ORIGINS behind proxies</h2>
<p>In production behind a proxy or load balancer, Django may see a different scheme or host than the browser used. If the Origin does not match a trusted pattern, the check fails even with a correct token. Add your real production origin — including <code>https://</code> — to <code>CSRF_TRUSTED_ORIGINS</code>.</p>
<h2>3. Cookie settings</h2>
<p>If the CSRF cookie never reaches the server, no token can validate. Check cookie domain and secure flags against how the site is actually served, and confirm the cookie is being set in the browser at all before blaming the form.</p>
<h2>4. The AJAX header</h2>
<p>JavaScript requests do not get the template tag automatically. Read the token from the cookie or the rendered page and send it in the <code>X-CSRFToken</code> header on every POST, PUT, and DELETE request.</p>
<h2>Read the full article</h2>
<p>This was a condensed summary. The complete troubleshooting guide is on the Djangix blog: <a href="https://djangix.com/blog/django-csrf-verification-failed/">Fixing 'CSRF Verification Failed' in Django</a></p>
]]></content:encoded></item><item><title><![CDATA[Django Tasks vs Celery: Which Should You Use?]]></title><description><![CDATA[Originally published on the Djangix blog. This is a condensed version — read the full article on djangix.com at the link below.
What Django's built-in Tasks framework gives you
Django now has a standa]]></description><link>https://djangix.hashnode.dev/django-tasks-vs-celery-which-should-you-use</link><guid isPermaLink="true">https://djangix.hashnode.dev/django-tasks-vs-celery-which-should-you-use</guid><category><![CDATA[Django]]></category><category><![CDATA[Python]]></category><category><![CDATA[celery]]></category><category><![CDATA[backend]]></category><dc:creator><![CDATA[Ubaid Ullah]]></dc:creator><pubDate>Fri, 09 Oct 2026 06:32:12 GMT</pubDate><content:encoded><![CDATA[<p><em>Originally published on the <a href="https://djangix.com/blog/django-tasks-vs-celery/">Djangix blog</a>. This is a condensed version — read the full article on djangix.com at the link below.</em></p>
<h2>What Django's built-in Tasks framework gives you</h2>
<p>Django now has a standard way to define and enqueue background tasks inside the project itself — no separate task library required just to get work off the request cycle. You define a task, enqueue it, and a backend runs it. For common jobs like sending email, generating thumbnails, cleaning up old data, or calling a slow third-party API, that simplicity is the point: fewer moving parts, less configuration, and an interface that feels like the rest of Django.</p>
<h2>Where Celery still wins</h2>
<p>Celery is the mature, battle-tested option, and it shows at scale. If you need advanced routing between queues, complex workflows made of chains and groups, fine-grained rate limiting and retries, periodic scheduling at volume, or a large ecosystem of brokers and monitoring tools, Celery gives you controls the built-in framework was never trying to provide. Teams already running Celery in production lose little by staying.</p>
<h2>The decision rule</h2>
<p>Start with Django Tasks when your background work is straightforward and moderate in volume, and you value doing more with what Django already ships. Choose Celery when the work itself is complex — multiple queues, chained workflows, heavy scheduling, or throughput that needs serious tuning — or when your team already knows it well.</p>
<p>In short: default to the simpler built-in option, and graduate to Celery when your requirements, not habit, demand it.</p>
<h2>Read the full article</h2>
<p>This was a condensed summary. The complete comparison is on the Djangix blog: <a href="https://djangix.com/blog/django-tasks-vs-celery/">Django Tasks vs Celery: Which Should You Use?</a></p>
]]></content:encoded></item><item><title><![CDATA[Verifying Stripe Webhooks in Django, Step by Step]]></title><description><![CDATA[Originally published on the Djangix blog. This is a condensed version — read the full article on djangix.com at the link below.
Why webhook signature verification matters
A Stripe webhook endpoint is ]]></description><link>https://djangix.hashnode.dev/verifying-stripe-webhooks-in-django-step-by-step</link><guid isPermaLink="true">https://djangix.hashnode.dev/verifying-stripe-webhooks-in-django-step-by-step</guid><category><![CDATA[Django]]></category><category><![CDATA[Python]]></category><category><![CDATA[stripe]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Ubaid Ullah]]></dc:creator><pubDate>Fri, 09 Oct 2026 06:30:49 GMT</pubDate><content:encoded><![CDATA[<p><em>Originally published on the <a href="https://djangix.com/blog/stripe-webhook-verification-django/">Djangix blog</a>. This is a condensed version — read the full article on djangix.com at the link below.</em></p>
<h2>Why webhook signature verification matters</h2>
<p>A Stripe webhook endpoint is just a public URL in your Django app. Anyone who finds that URL can POST to it — including someone sending a forged “payment succeeded” event to get an order marked as paid without ever paying. Signature verification is how you prove an event really came from Stripe before you act on it.</p>
<h2>The raw-body requirement</h2>
<p>Stripe calculates the signature over the exact bytes it sent. If you parse the JSON first and re-serialize it, the bytes change — key ordering and whitespace are not preserved — and verification fails even for genuine events. In Django, read <code>request.body</code> first and pass those raw bytes to Stripe; do not work from <code>json.loads()</code> output or <code>request.POST</code> for this check.</p>
<h2>Verifying with the signing secret</h2>
<p>Each webhook endpoint has its own signing secret (starting with <code>whsec_</code>). Take three things — the raw request body, the <code>Stripe-Signature</code> header, and that secret — and pass them to Stripe’s event construction/verification helper. If it raises an error, reject the request. Only process the event after verification succeeds.</p>
<h2>Three common pitfalls</h2>
<ol>
<li><strong>Parsed JSON instead of raw bytes</strong> — verifying against a decoded and re-encoded payload instead of <code>request.body</code>.</li>
<li><strong>The wrong secret in production</strong> — test and live endpoints have different signing secrets, and a stale environment variable will fail every real event.</li>
<li><strong>Not returning 200 fast</strong> — slow handlers trigger Stripe retries and duplicate deliveries. Verify, acknowledge with a 200 quickly, and move heavy work out of the request path.</li>
</ol>
<h2>Read the full article</h2>
<p>This was a condensed summary. The complete walkthrough, with full code, is on the Djangix blog: <a href="https://djangix.com/blog/stripe-webhook-verification-django/">Verifying Stripe Webhooks in Django, Step by Step</a></p>
]]></content:encoded></item></channel></rss>