<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Julian Halden</title>
    <description>The latest articles on DEV Community by Julian Halden (@julianhalden).</description>
    <link>https://dev.to/julianhalden</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4143599%2F7bca5d3a-3889-4f25-a43a-e47ce4c7a315.png</url>
      <title>DEV Community: Julian Halden</title>
      <link>https://dev.to/julianhalden</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/julianhalden"/>
    <language>en</language>
    <item>
      <title>WebAuthn RP ID vs Origin: Fixing Cross-Domain Passkeys</title>
      <dc:creator>Julian Halden</dc:creator>
      <pubDate>Sat, 24 Oct 2026 04:42:24 +0000</pubDate>
      <link>https://dev.to/julianhalden/webauthn-rp-id-vs-origin-fixing-cross-domain-passkeys-4p30</link>
      <guid>https://dev.to/julianhalden/webauthn-rp-id-vs-origin-fixing-cross-domain-passkeys-4p30</guid>
      <description>&lt;p&gt;If your passkey works on one hostname but fails on another, start by comparing the &lt;strong&gt;RP ID&lt;/strong&gt; with the &lt;strong&gt;actual calling origin&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;They are not interchangeable values. An RP ID is a domain identifier. An origin includes the scheme, hostname, and port.&lt;/p&gt;

&lt;p&gt;This is part three of &lt;strong&gt;Passkeys in Production&lt;/strong&gt;, adapted from the &lt;a href="https://helpers.work/blog/webauthn-rp-id-related-origins" rel="noopener noreferrer"&gt;Helpers.work RP ID guide&lt;/a&gt;. We build the &lt;a href="https://helpers.work/tool/passkey-checker" rel="noopener noreferrer"&gt;Passkey Deployment Checker&lt;/a&gt;, which evaluates this public configuration boundary without claiming to test the entire login flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  One URL, three different values
&lt;/h2&gt;

&lt;p&gt;For this illustrative page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://login.example.com/account/security
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Origin:   https://login.example.com
Hostname: login.example.com
RP ID:    potentially example.com or login.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The path is not part of the origin. A non-default port is part of the origin, but never part of the RP ID.&lt;/p&gt;

&lt;p&gt;Registration options use &lt;code&gt;publicKey.rp.id&lt;/code&gt;; authentication options use &lt;code&gt;publicKey.rpId&lt;/code&gt;. The server's expected RP ID must agree with the credential's scope.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fufj3t1kql7xioownl56b.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fufj3t1kql7xioownl56b.webp" alt="The browser origin https://login.example.com includes a scheme and host, while its eligible parent RP ID example.com is a bare domain." width="800" height="603"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The ordinary relationship is directional
&lt;/h2&gt;

&lt;p&gt;Without Related Origin Requests, a caller can use its own effective domain or an eligible parent domain as the RP ID.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Caller&lt;/th&gt;
&lt;th&gt;RP ID&lt;/th&gt;
&lt;th&gt;Ordinary relationship&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;https://login.example.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;example.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Eligible parent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;https://login.example.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;login.example.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same host&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;https://example.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;login.example.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Not an eligible parent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;https://shop.example.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;login.example.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sibling, not parent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;https://example.co.uk&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;example.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Different domain&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A public suffix such as &lt;code&gt;com&lt;/code&gt; or &lt;code&gt;co.uk&lt;/code&gt; is not an acceptable shared RP ID.&lt;/p&gt;

&lt;p&gt;This is why "just remove the first subdomain" is not a sufficient validation algorithm. Public suffix handling needs standards-aware logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sibling subdomains do not always need a JSON file
&lt;/h2&gt;

&lt;p&gt;If &lt;code&gt;accounts.example.com&lt;/code&gt; and &lt;code&gt;shop.example.com&lt;/code&gt; both use credentials registered for &lt;code&gt;example.com&lt;/code&gt;, the ordinary relationship can be enough.&lt;/p&gt;

&lt;p&gt;You still need:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Credentials created for that exact RP ID.&lt;/li&gt;
&lt;li&gt;Correct shared account and credential mappings.&lt;/li&gt;
&lt;li&gt;Explicit server acceptance of the intended origins.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A broad RP ID does not authorize every subdomain in your backend. Treat delegated, abandoned, and user-controlled subdomains as part of the threat model.&lt;/p&gt;

&lt;p&gt;Cookies and shared sessions remain separate concerns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Different domains need explicit authorization
&lt;/h2&gt;

&lt;p&gt;Suppose the browser runs at &lt;code&gt;https://example.co.uk&lt;/code&gt; but requests RP ID &lt;code&gt;example.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A supporting browser can use Related Origin Requests if the RP ID domain publishes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/.well-known/webauthn
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"origins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"https://example.co.uk"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://login.example.net"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are illustrative origins. Serve your actual document publicly as JSON and list only relationships you intend to trust.&lt;/p&gt;

&lt;p&gt;The location matters: putting this file on &lt;code&gt;example.co.uk&lt;/code&gt; does not authorize use of the &lt;code&gt;example.com&lt;/code&gt; RP ID.&lt;/p&gt;

&lt;h2&gt;
  
  
  The server still verifies the actual origin
&lt;/h2&gt;

&lt;p&gt;The public allowlist permits the browser to request the credential. It does not replace the relying party's verification policy.&lt;/p&gt;

&lt;p&gt;For the example above, the server must explicitly accept &lt;code&gt;https://example.co.uk&lt;/code&gt;, verify the expected challenge and RP ID context, and validate the appropriate cryptographic response.&lt;/p&gt;

&lt;p&gt;Do not fix a mismatch by accepting arbitrary origins. CORS headers and cookie-domain changes also do not substitute for WebAuthn authorization.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhpolhsliocvj3wphckw6.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhpolhsliocvj3wphckw6.webp" alt="Cross-domain flow: example.co.uk requests RP ID example.com, the related origins file is served on example.com, and the backend still verifies the caller." width="800" height="627"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Five labels is not five URLs
&lt;/h2&gt;

&lt;p&gt;The compatibility procedure counts registrable domain labels:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;example.com&lt;/code&gt; contributes &lt;code&gt;example&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;example.co.uk&lt;/code&gt; also contributes &lt;code&gt;example&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;login.example.net&lt;/code&gt; also contributes &lt;code&gt;example&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;example-rewards.com&lt;/code&gt; contributes &lt;code&gt;example-rewards&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Supporting implementations must handle at least five distinct labels; they may support more. The checker evaluates that five-label baseline.&lt;/p&gt;

&lt;p&gt;Order matters when a client exhausts its label budget. A listed origin can still fall beyond the supported limit.&lt;/p&gt;

&lt;p&gt;Do not publish a giant list and assume every client will process all of it. For many unrelated tenant domains, a central sign-in domain may be the more maintainable design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debug in this order
&lt;/h2&gt;

&lt;p&gt;First, inspect the actual origin where WebAuthn runs. A redirect from a marketing page can change the relevant hostname.&lt;/p&gt;

&lt;p&gt;Second, inspect the backend-generated options. Do not assume the RP ID was discovered by a scanner; the Helpers.work tool defaults to the hostname when the RP ID is omitted.&lt;/p&gt;

&lt;p&gt;Third, determine whether ordinary parent-domain eligibility is enough. If not, inspect the RP ID host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--proto&lt;/span&gt; &lt;span class="s1"&gt;'=https'&lt;/span&gt; &lt;span class="nt"&gt;--max-time&lt;/span&gt; 15 &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="nt"&gt;--show-error&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://example.com/.well-known/webauthn
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the example domain with yours. Look for HTML fallbacks, login challenges, incorrect content types, or missing origins.&lt;/p&gt;

&lt;p&gt;Finally, test browser support and server verification. Basic WebAuthn support does not guarantee Related Origin Requests support. A &lt;code&gt;SecurityError&lt;/code&gt; is a diagnostic clue, not a complete root-cause report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Domain changes are credential migrations
&lt;/h2&gt;

&lt;p&gt;Existing credentials do not change RP ID when you update configuration.&lt;/p&gt;

&lt;p&gt;Before renaming an authentication domain, decide whether to retain the old RP ID through a supported related-origin flow or guide users through authenticated re-enrollment. Keep a tested path for unsupported clients.&lt;/p&gt;

&lt;p&gt;Continue with &lt;a href="https://helpers.work/blog/passkey-deployment-checker-guide" rel="noopener noreferrer"&gt;reading the checker report&lt;/a&gt;. For the broader rollout, see the &lt;a href="https://helpers.work/blog/passkey-deployment-checklist" rel="noopener noreferrer"&gt;deployment checklist&lt;/a&gt; and &lt;a href="https://helpers.work/blog/passkeys-explained" rel="noopener noreferrer"&gt;passkeys overview&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;References: &lt;a href="https://www.w3.org/TR/webauthn-3/#sctn-related-origins" rel="noopener noreferrer"&gt;W3C related origins&lt;/a&gt; and &lt;a href="https://web.dev/articles/webauthn-related-origin-requests" rel="noopener noreferrer"&gt;web.dev's implementation guide&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>authentication</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>A Passkey Deployment Checklist Beyond the Happy-Path Demo</title>
      <dc:creator>Julian Halden</dc:creator>
      <pubDate>Sat, 24 Oct 2026 04:42:16 +0000</pubDate>
      <link>https://dev.to/julianhalden/a-passkey-deployment-checklist-beyond-the-happy-path-demo-4dba</link>
      <guid>https://dev.to/julianhalden/a-passkey-deployment-checklist-beyond-the-happy-path-demo-4dba</guid>
      <description>&lt;p&gt;Creating a passkey once on your laptop is not the same as deploying passkeys.&lt;/p&gt;

&lt;p&gt;A production release needs evidence at three layers: public configuration, server verification, and the real user journey. A failure at any layer can produce a broken experience even when the other two look healthy.&lt;/p&gt;

&lt;p&gt;This is part two of &lt;strong&gt;Passkeys in Production&lt;/strong&gt;, adapted from our &lt;a href="https://helpers.work/blog/passkey-deployment-checklist" rel="noopener noreferrer"&gt;full deployment checklist&lt;/a&gt;. Helpers.work also provides the &lt;a href="https://helpers.work/tool/passkey-checker" rel="noopener noreferrer"&gt;Passkey Deployment Checker&lt;/a&gt; used for the public-configuration layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Record the identity boundary
&lt;/h2&gt;

&lt;p&gt;Write down the actual sign-in origin, RP ID, allowed server origins, credential store, and native app identifiers.&lt;/p&gt;

&lt;p&gt;An illustrative setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sign-in origin: https://login.example.com
RP ID:          example.com
Server policy:  explicitly allow https://login.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The RP ID is not necessarily the hostname visible in the address bar. Read it from the options your backend produces.&lt;/p&gt;

&lt;p&gt;Keep staging and production boundaries intentional. A parent-domain RP ID combined with overly broad server origin acceptance can expose production authentication to weaker environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Check public HTTPS
&lt;/h2&gt;

&lt;p&gt;Verify the real login hostname, not only the marketing site:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DNS resolves to the intended service.&lt;/li&gt;
&lt;li&gt;The certificate covers that hostname.&lt;/li&gt;
&lt;li&gt;The server sends a complete chain.&lt;/li&gt;
&lt;li&gt;HTTPS redirects stay on HTTPS.&lt;/li&gt;
&lt;li&gt;Renewal and expiry monitoring are working.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://helpers.work/tool/ssl-checker" rel="noopener noreferrer"&gt;SSL Certificate Checker&lt;/a&gt; can help identify certificate problems. A valid certificate on the apex domain is not proof that every login subdomain is configured correctly.&lt;/p&gt;

&lt;p&gt;Localhost development exceptions do not make HTTP production suitable. A public checker also should not be used to scan private infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Publish only the files you need
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Deployment feature&lt;/th&gt;
&lt;th&gt;Relevant configuration&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ordinary single-domain web sign-in&lt;/td&gt;
&lt;td&gt;HTTPS, eligible RP ID, server verification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-domain use of one RP ID&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/.well-known/webauthn&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Credential-manager discovery&lt;/td&gt;
&lt;td&gt;Optional &lt;code&gt;/.well-known/passkey-endpoints&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Native Apple app&lt;/td&gt;
&lt;td&gt;Associated Domains and &lt;code&gt;webcredentials&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Native Android app&lt;/td&gt;
&lt;td&gt;Digital Asset Links and release signing identity&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sibling subdomains using an eligible common parent RP ID do not normally need Related Origin Requests. They still need explicit server origin validation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0rw21q21f2lzwydbkasm.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0rw21q21f2lzwydbkasm.webp" alt="Configuration map showing when web sign-in, related origins, optional passkey endpoints, Apple associations, and Android asset links apply." width="800" height="627"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Inspect the deployed response
&lt;/h2&gt;

&lt;p&gt;A reverse proxy can turn a well-known request into a redirect or an HTML application shell. Inspect status, headers, and body together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--proto&lt;/span&gt; &lt;span class="s1"&gt;'=https'&lt;/span&gt; &lt;span class="nt"&gt;--max-time&lt;/span&gt; 15 &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="nt"&gt;--show-error&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://example.com/.well-known/passkey-endpoints
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the example domain with your RP ID. This command leaves redirects visible instead of following them.&lt;/p&gt;

&lt;p&gt;An illustrative discovery document:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"enroll"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.com/account/passkeys/new"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"manage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.com/account/passkeys"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The JSON should be publicly retrievable. The account pages themselves can require authentication and must enforce your enrollment policy.&lt;/p&gt;

&lt;p&gt;These are user-facing discovery links, not WebAuthn verification APIs. An absent passkey-endpoints document does not by itself break sign-in.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Verify mobile release identity
&lt;/h2&gt;

&lt;p&gt;For Apple, the public association file and the app's entitlement must agree. Check &lt;code&gt;webcredentials&lt;/code&gt;, not just &lt;code&gt;applinks&lt;/code&gt;, and preserve existing association sections.&lt;/p&gt;

&lt;p&gt;For Android, compare the package name and signing-certificate fingerprint for the build users actually install. A debug certificate or upload certificate can differ from the distribution certificate.&lt;/p&gt;

&lt;p&gt;The credential-sharing relation is &lt;code&gt;delegate_permission/common.get_login_creds&lt;/code&gt;. Follow the &lt;a href="https://developer.android.com/identity/credential-manager/prerequisites" rel="noopener noreferrer"&gt;Android prerequisites&lt;/a&gt; for the surrounding configuration.&lt;/p&gt;

&lt;p&gt;A public file scanner cannot inspect your installed binary or force platform association caches to refresh. Test the distributed app.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Exercise the backend rejection paths
&lt;/h2&gt;

&lt;p&gt;Use a maintained verifier, then test your own wiring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unexpected, expired, and replayed challenges are rejected.&lt;/li&gt;
&lt;li&gt;Unapproved origins are rejected.&lt;/li&gt;
&lt;li&gt;The RP ID context must match.&lt;/li&gt;
&lt;li&gt;Required presence and verification flags are enforced.&lt;/li&gt;
&lt;li&gt;A credential cannot authenticate a different account.&lt;/li&gt;
&lt;li&gt;Revoked credentials cannot start a new session.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Challenges should be bound to the intended operation and consumed reliably. Do not allow concurrent requests to make a completed challenge reusable.&lt;/p&gt;

&lt;p&gt;Be careful with signature counters. Some authenticators and synced credentials do not supply a useful monotonic counter. Apply the library's documented policy rather than treating every non-increase as definitive evidence of cloning.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Test the lifecycle
&lt;/h2&gt;

&lt;p&gt;First enrollment is only the start. Test sign-out, sign-in, prompt cancellation, second-credential enrollment, revocation, device loss, and recovery.&lt;/p&gt;

&lt;p&gt;Include the browsers, operating systems, credential providers, and mobile builds your audience actually uses. Conditional UI and related-origin support can differ from basic WebAuthn support.&lt;/p&gt;

&lt;p&gt;Keep fallback explicit. An unsupported client should not trigger a hidden bypass of origin or verification policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Plan rollback without locking users out
&lt;/h2&gt;

&lt;p&gt;Stopping new enrollments and stopping existing passkey authentication are different changes. During an incident, the second can strand users who no longer have another sign-in method.&lt;/p&gt;

&lt;p&gt;Preserve credential records and the RP ID through an ordinary frontend rollback. Test recovery before removing passwords, and decide which recovered sessions may immediately enroll a new credential.&lt;/p&gt;

&lt;p&gt;Track aggregate completion and failure categories without collecting raw credential payloads as routine analytics.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcpz5xo7dgnuavg07ddzk.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcpz5xo7dgnuavg07ddzk.webp" alt="Four passkey release gates: infrastructure, identity configuration, server verification, and the complete user lifecycle." width="800" height="596"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The release gate
&lt;/h2&gt;

&lt;p&gt;Run the &lt;a href="https://helpers.work/tool/passkey-checker" rel="noopener noreferrer"&gt;Passkey Deployment Checker&lt;/a&gt; with your actual RP ID and expected app values. Then complete server and real-device tests.&lt;/p&gt;

&lt;p&gt;No public scan can prove your challenge storage, account mapping, session issuance, or recovery process is correct.&lt;/p&gt;

&lt;p&gt;Continue with &lt;a href="https://helpers.work/blog/webauthn-rp-id-related-origins" rel="noopener noreferrer"&gt;RP ID and related origins&lt;/a&gt;, or use the &lt;a href="https://helpers.work/blog/passkey-deployment-checker-guide" rel="noopener noreferrer"&gt;report interpretation guide&lt;/a&gt;. New to the model? Start with &lt;a href="https://helpers.work/blog/passkeys-explained" rel="noopener noreferrer"&gt;passkeys explained&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;References: &lt;a href="https://www.w3.org/TR/webauthn-3/" rel="noopener noreferrer"&gt;W3C WebAuthn&lt;/a&gt;, &lt;a href="https://www.w3.org/TR/passkey-endpoints/" rel="noopener noreferrer"&gt;passkey endpoints&lt;/a&gt;, and &lt;a href="https://developer.apple.com/documentation/xcode/supporting-associated-domains" rel="noopener noreferrer"&gt;Apple associated domains&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>devops</category>
      <category>authentication</category>
    </item>
    <item>
      <title>Passkeys Explained: What Changes Behind the Sign-In Button</title>
      <dc:creator>Julian Halden</dc:creator>
      <pubDate>Sat, 24 Oct 2026 04:42:07 +0000</pubDate>
      <link>https://dev.to/julianhalden/passkeys-explained-what-changes-behind-the-sign-in-button-b96</link>
      <guid>https://dev.to/julianhalden/passkeys-explained-what-changes-behind-the-sign-in-button-b96</guid>
      <description>&lt;p&gt;A passkey is not your fingerprint stored on a website. It is a public-key credential, and the fingerprint prompt is one possible way to authorize its use locally.&lt;/p&gt;

&lt;p&gt;That distinction explains both the appeal of passkeys and the engineering work that remains after adding a sign-in button.&lt;/p&gt;

&lt;p&gt;This is part one of &lt;strong&gt;Passkeys in Production&lt;/strong&gt;, adapted from the &lt;a href="https://helpers.work/blog/passkeys-explained" rel="noopener noreferrer"&gt;Helpers.work passkeys guide&lt;/a&gt;. We build the &lt;a href="https://helpers.work/tool/passkey-checker" rel="noopener noreferrer"&gt;Passkey Deployment Checker&lt;/a&gt;, so this series also explains what a public configuration check can and cannot establish.&lt;/p&gt;

&lt;h2&gt;
  
  
  The smallest useful mental model
&lt;/h2&gt;

&lt;p&gt;With a password, the user presents a reusable secret. With a passkey, an authenticator answers a fresh challenge using a private key, and your server verifies the response using a public key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your server:       challenge + authentication options
Browser:          asks the authenticator for a credential
Authenticator:    user approval + signed response
Your server:       verification + application session
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The website never needs the private key. It does need a reliable mapping between credential IDs, public keys, and accounts.&lt;/p&gt;

&lt;p&gt;Registration creates that mapping. Authentication uses it. Do not treat registration as a harmless settings change: enrolling a new credential gives someone a new way to enter the account.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqc2stth4sfnamp13e99e.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqc2stth4sfnamp13e99e.webp" alt="Passkey authentication in four steps: issue a challenge, approve with an authenticator, verify the response, and create an application session." width="800" height="620"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What WebAuthn does
&lt;/h2&gt;

&lt;p&gt;On the web, WebAuthn exposes credential creation and authentication through the browser. The browser and authenticator enforce the credential's relying party scope.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;RP ID&lt;/strong&gt; is a domain identifier, such as &lt;code&gt;example.com&lt;/code&gt;. It is not the same as the browser origin, such as &lt;code&gt;https://login.example.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Your backend must verify the challenge, expected origin, RP ID context, applicable user-presence and user-verification requirements, and the appropriate cryptographic response. For authentication, that includes the assertion signature against the correct stored public key.&lt;/p&gt;

&lt;p&gt;A signature-only implementation is incomplete. Use a maintained WebAuthn server library and test your configuration around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is harder to phish
&lt;/h2&gt;

&lt;p&gt;A lookalike domain cannot normally request credentials for your RP ID. The person does not have to inspect a convincing login page and decide where to type a reusable secret.&lt;/p&gt;

&lt;p&gt;That is a different property from a one-time code. Someone can copy an OTP into a phishing page. A correctly scoped credential is not a code that can be pasted anywhere.&lt;/p&gt;

&lt;p&gt;There are deliberate cross-domain mechanisms, including Related Origin Requests. They need explicit configuration, not a permissive origin wildcard.&lt;/p&gt;

&lt;p&gt;Phishing resistance is not total account security. A stolen session, a compromised server, unsafe credential enrollment, and weak recovery can still put an account at risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Synced does not mean universal
&lt;/h2&gt;

&lt;p&gt;Some passkeys synchronize through a credential provider. Others are bound to an authenticator, such as a hardware security key.&lt;/p&gt;

&lt;p&gt;These models produce different support questions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Credential model&lt;/th&gt;
&lt;th&gt;Question to test&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Synced&lt;/td&gt;
&lt;td&gt;Is the credential available through the user's chosen provider on the next device?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Device-bound&lt;/td&gt;
&lt;td&gt;What happens if the authenticator is lost or damaged?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-device authentication&lt;/td&gt;
&lt;td&gt;Can the user complete the intended approval flow from another device?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A QR-code flow does not necessarily copy a passkey to the computer. It may let another device perform authentication.&lt;/p&gt;

&lt;p&gt;Likewise, "supports passkeys" does not promise the same autofill or cross-device experience in every browser.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffjay42fdwsnfh774wgrh.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffjay42fdwsnfh774wgrh.webp" alt="Synced passkeys use a provider, device-bound passkeys stay with an authenticator, and cross-device authentication does not necessarily copy a credential." width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Is a passkey automatically MFA?
&lt;/h2&gt;

&lt;p&gt;Not every WebAuthn operation has the same assurance properties. Local user verification can combine an authenticator with a PIN or biometric check, but a touch-only interaction has different semantics.&lt;/p&gt;

&lt;p&gt;Specify the policy you need, request the appropriate verification behavior, and verify it on the server. Do not assume that the presence of a security-key prompt satisfies every organizational MFA requirement.&lt;/p&gt;

&lt;p&gt;The website receives a cryptographic response, not a fingerprint image. That is an important privacy distinction to explain in onboarding copy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part the demo usually leaves out
&lt;/h2&gt;

&lt;p&gt;Before rolling out, test more than first registration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sign out and authenticate with a fresh challenge.&lt;/li&gt;
&lt;li&gt;Cancel the prompt and recover gracefully.&lt;/li&gt;
&lt;li&gt;Add a second credential.&lt;/li&gt;
&lt;li&gt;Remove a credential and reject its future use.&lt;/li&gt;
&lt;li&gt;Recover after losing the only available authenticator.&lt;/li&gt;
&lt;li&gt;Try an unsupported browser or device.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The resulting session needs the same care as any other authenticated session. Passkeys do not fix authorization bugs or unsafe cookies.&lt;/p&gt;

&lt;p&gt;Use user-facing labels such as "Create a passkey" instead of "Scan your fingerprint." A user's actual authenticator may use a PIN, face recognition, or a security key.&lt;/p&gt;

&lt;h2&gt;
  
  
  You do not need every well-known file
&lt;/h2&gt;

&lt;p&gt;A web-only deployment with an eligible RP ID can work without native app association files.&lt;/p&gt;

&lt;p&gt;Related origins matter when different domains need one RP ID. Apple and Android association files matter for the relevant native apps. Passkey endpoints are optional metadata that helps clients discover enrollment and management pages.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://helpers.work/tool/passkey-checker" rel="noopener noreferrer"&gt;Passkey Deployment Checker&lt;/a&gt; inspects public configuration. It cannot register a credential for your account, audit your backend verifier, or prove recovery works. A clean report is one piece of evidence, not the final release gate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Continue the series
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://helpers.work/blog/passkeys-explained" rel="noopener noreferrer"&gt;Passkeys explained&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://helpers.work/blog/passkey-deployment-checklist" rel="noopener noreferrer"&gt;Production deployment checklist&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://helpers.work/blog/webauthn-rp-id-related-origins" rel="noopener noreferrer"&gt;RP ID and related origins&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://helpers.work/blog/passkey-deployment-checker-guide" rel="noopener noreferrer"&gt;Reading a deployment checker report&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For the underlying API and verification requirements, see &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API" rel="noopener noreferrer"&gt;MDN's Web Authentication overview&lt;/a&gt; and the &lt;a href="https://www.w3.org/TR/webauthn-3/" rel="noopener noreferrer"&gt;W3C WebAuthn specification&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>authentication</category>
      <category>beginners</category>
    </item>
    <item>
      <title>How to Read a Passkey Deployment Checker Report</title>
      <dc:creator>Julian Halden</dc:creator>
      <pubDate>Sat, 24 Oct 2026 04:41:56 +0000</pubDate>
      <link>https://dev.to/julianhalden/how-to-read-a-passkey-deployment-checker-report-4hb8</link>
      <guid>https://dev.to/julianhalden/how-to-read-a-passkey-deployment-checker-report-4hb8</guid>
      <description>&lt;p&gt;A website can have valid passkey metadata and a broken authentication flow. It can also support passkeys while publishing none of the optional discovery files a scanner looks for.&lt;/p&gt;

&lt;p&gt;The useful question is not "How many green checks do I have?" It is "Which conditions did this report evaluate, and which of them does my architecture require?"&lt;/p&gt;

&lt;p&gt;This is the final part of &lt;strong&gt;Passkeys in Production&lt;/strong&gt;, adapted from the &lt;a href="https://helpers.work/blog/passkey-deployment-checker-guide" rel="noopener noreferrer"&gt;full report guide&lt;/a&gt;. We build the &lt;a href="https://helpers.work/tool/passkey-checker" rel="noopener noreferrer"&gt;Helpers.work Passkey Deployment Checker&lt;/a&gt;; this walkthrough describes its actual scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use the origin where WebAuthn runs
&lt;/h2&gt;

&lt;p&gt;Enter your public sign-in URL or domain. The tool normalizes it to an origin, so supplying &lt;code&gt;/account/security&lt;/code&gt; does not turn the scan into a test of that page.&lt;/p&gt;

&lt;p&gt;If the marketing site redirects to a login host, use the host where the browser actually creates or requests credentials.&lt;/p&gt;

&lt;p&gt;The checker uses public HTTPS domains on port 443. It does not scan localhost, IP-literal targets, private networks, custom website ports, or URLs containing embedded credentials.&lt;/p&gt;

&lt;p&gt;Those are tool restrictions. They are not a claim that every excluded environment is inherently unsupported by WebAuthn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter your real RP ID
&lt;/h2&gt;

&lt;p&gt;The most important advanced field is the RP ID.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Website: https://login.example.com
RP ID:   example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This illustrative setup may be intentional. If you omit the RP ID, the tool instead uses &lt;code&gt;login.example.com&lt;/code&gt; and marks the source as &lt;code&gt;inferred&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is a default, not reliable discovery. Read &lt;code&gt;publicKey.rp.id&lt;/code&gt; in registration options and &lt;code&gt;publicKey.rpId&lt;/code&gt; in authentication options from your backend.&lt;/p&gt;

&lt;p&gt;See the &lt;a href="https://helpers.work/blog/webauthn-rp-id-related-origins" rel="noopener noreferrer"&gt;RP ID guide&lt;/a&gt; if the domain relationship is unclear.&lt;/p&gt;

&lt;h2&gt;
  
  
  Know the six checks
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;What it evaluates&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HTTPS Configuration&lt;/td&gt;
&lt;td&gt;Public connectivity and normal certificate validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RP ID Compatibility&lt;/td&gt;
&lt;td&gt;Website-to-RP relationship for the supplied inputs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Related Origins&lt;/td&gt;
&lt;td&gt;RP-host &lt;code&gt;/.well-known/webauthn&lt;/code&gt; and applicable origins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Passkey Endpoints&lt;/td&gt;
&lt;td&gt;Optional discovery metadata and URL syntax&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apple App Site Association&lt;/td&gt;
&lt;td&gt;Public &lt;code&gt;webcredentials&lt;/code&gt; declarations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Android Asset Links&lt;/td&gt;
&lt;td&gt;Public credential-sharing statements and expected identity&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;HTTPS and RP ID checks are always evaluated. Related origins and passkey endpoints are selected by default; mobile checks are opt-in.&lt;/p&gt;

&lt;p&gt;An unrelated RP ID forces the required related-origin evaluation even if the optional selection is off.&lt;/p&gt;

&lt;p&gt;For native apps, provide expected identifiers and the actual release signing-certificate fingerprint when available. Without expected values, valid JSON for another app is not proof that your app is configured.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fze5389eh8r6pd02avwtm.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fze5389eh8r6pd02avwtm.webp" alt="The six public configuration checks cover HTTPS, RP ID, related origins, passkey endpoints, Apple associations, and Android links, not actual sign-in." width="800" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Read statuses instead of inventing a score
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;passed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The evaluated condition passed for these inputs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;warning&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A concern or compatibility limitation needs review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;failed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The evaluated condition failed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;not_applicable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Not selected, or optional configuration is not applicable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unknown&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The checker could not reach a reliable conclusion&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The report-level &lt;code&gt;success&lt;/code&gt; value means a report was produced. It does not mean all checks passed.&lt;/p&gt;

&lt;p&gt;A required related-origin failure can block your intended cross-domain flow. A missing optional discovery document may not affect sign-in at all. A network timeout should not be relabeled as either a pass or a confirmed deployment defect.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz12o878u5gykc9fj9yjl.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz12o878u5gykc9fj9yjl.webp" alt="The five report statuses are passed, warning, failed, not_applicable, and unknown; report success does not mean passkey sign-in succeeds." width="800" height="643"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Three examples of useful interpretation
&lt;/h2&gt;

&lt;p&gt;These are hypothetical scenarios, not live scan results.&lt;/p&gt;

&lt;h3&gt;
  
  
  The web-only application
&lt;/h3&gt;

&lt;p&gt;The site uses an eligible RP ID and has no native apps. Apple and Android association files are not relevant to that deployment.&lt;/p&gt;

&lt;p&gt;A missing &lt;code&gt;/.well-known/passkey-endpoints&lt;/code&gt; file is also not proof that passkeys are unavailable. It is optional metadata for helping clients find enrollment and management pages.&lt;/p&gt;

&lt;p&gt;Focus on the actual registration and authentication ceremony.&lt;/p&gt;

&lt;h3&gt;
  
  
  The cross-domain application
&lt;/h3&gt;

&lt;p&gt;The browser runs at &lt;code&gt;https://example.co.uk&lt;/code&gt; and requests RP ID &lt;code&gt;example.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Inspect &lt;code&gt;https://example.com/.well-known/webauthn&lt;/code&gt;. Verify the calling origin is represented, the body is usable JSON, and the client supports the mechanism.&lt;/p&gt;

&lt;p&gt;The backend still needs explicit origin acceptance and the correct credential records.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Android release-only failure
&lt;/h3&gt;

&lt;p&gt;The public file may contain a debug or upload-certificate fingerprint rather than the certificate on the distributed app.&lt;/p&gt;

&lt;p&gt;Compare package name, credential-sharing relation, and release signing identity together. The scanner cannot inspect the installed binary or app manifest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some green results have important limits
&lt;/h2&gt;

&lt;p&gt;A passkey-endpoints result validates advertised URLs syntactically. It does not visit those URLs or exercise enrollment.&lt;/p&gt;

&lt;p&gt;An Apple association result does not verify the app entitlement or Apple's cached state. An Android result does not verify the manifest, resources, or installed signing certificate.&lt;/p&gt;

&lt;p&gt;An HTTPS result is not a full TLS audit. It does not evaluate every cipher, security header, or route.&lt;/p&gt;

&lt;p&gt;And no public configuration report verifies challenge freshness, assertion signatures, account mapping, session issuance, or account recovery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix the failed layer, then test again
&lt;/h2&gt;

&lt;p&gt;Work from DNS and HTTPS toward RP ID, required files, clients, and the server ceremony.&lt;/p&gt;

&lt;p&gt;An HTML response with status &lt;code&gt;200&lt;/code&gt; often points to a catch-all route, not malformed JSON. Redirect handling also differs: this checker follows bounded HTTPS redirects for related origins but treats passkey-endpoint and app-association redirects differently.&lt;/p&gt;

&lt;p&gt;After deployment, inspect the public response rather than assuming the local file is already visible through the CDN.&lt;/p&gt;

&lt;p&gt;Then register, sign out, sign in, cancel, revoke, and recover on your supported clients. Use the &lt;a href="https://helpers.work/blog/passkey-deployment-checklist" rel="noopener noreferrer"&gt;production checklist&lt;/a&gt; as the final release gate.&lt;/p&gt;

&lt;p&gt;For the conceptual foundation, see &lt;a href="https://helpers.work/blog/passkeys-explained" rel="noopener noreferrer"&gt;passkeys explained&lt;/a&gt;. Technical references: &lt;a href="https://www.w3.org/TR/passkey-endpoints/" rel="noopener noreferrer"&gt;W3C passkey endpoints&lt;/a&gt;, &lt;a href="https://developer.apple.com/documentation/xcode/supporting-associated-domains" rel="noopener noreferrer"&gt;Apple associated domains&lt;/a&gt;, and &lt;a href="https://developer.android.com/identity/credential-manager/prerequisites" rel="noopener noreferrer"&gt;Android prerequisites&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Unix Timestamps Explained: Seconds, Milliseconds, Time Zones and Common Mistakes</title>
      <dc:creator>Julian Halden</dc:creator>
      <pubDate>Fri, 25 Sep 2026 23:56:59 +0000</pubDate>
      <link>https://dev.to/julianhalden/unix-timestamps-explained-seconds-milliseconds-time-zones-and-common-mistakes-1p9c</link>
      <guid>https://dev.to/julianhalden/unix-timestamps-explained-seconds-milliseconds-time-zones-and-common-mistakes-1p9c</guid>
      <description>&lt;p&gt;Unix timestamps are everywhere. APIs, databases, logs, JavaScript and authentication tokens all use them, and they cause a surprising number of bugs.&lt;/p&gt;

&lt;p&gt;Is &lt;code&gt;1790186400&lt;/code&gt; a date? What about &lt;code&gt;1790186400000&lt;/code&gt;? Does a Unix timestamp contain a time zone? Why does JavaScript sometimes show a date in your local time when the timestamp itself is supposed to be UTC?&lt;/p&gt;

&lt;p&gt;This article explains how Unix timestamps actually work, the difference between seconds and milliseconds, how time zones fit into the picture, and the mistakes developers most commonly make.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;What is a Unix timestamp?&lt;/li&gt;
&lt;li&gt;Seconds vs milliseconds&lt;/li&gt;
&lt;li&gt;JavaScript uses milliseconds&lt;/li&gt;
&lt;li&gt;Does a Unix timestamp have a time zone?&lt;/li&gt;
&lt;li&gt;UTC vs local time&lt;/li&gt;
&lt;li&gt;ISO 8601 and Unix timestamps are different things&lt;/li&gt;
&lt;li&gt;Common mistakes

&lt;ul&gt;
&lt;li&gt;#1: Mixing seconds and milliseconds&lt;/li&gt;
&lt;li&gt;#2: Assuming the timestamp is in local time&lt;/li&gt;
&lt;li&gt;#3: Removing the UTC indicator&lt;/li&gt;
&lt;li&gt;#4: Manually applying time-zone offsets&lt;/li&gt;
&lt;li&gt;#5: Ignoring daylight saving time&lt;/li&gt;
&lt;li&gt;#6: Storing local time instead of an instant&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Not every date should be a Unix timestamp&lt;/li&gt;
&lt;li&gt;Negative Unix timestamps&lt;/li&gt;
&lt;li&gt;What about the Year 2038 problem?&lt;/li&gt;
&lt;li&gt;Notable Unix timestamps&lt;/li&gt;
&lt;li&gt;Unix timestamp precision&lt;/li&gt;
&lt;li&gt;Converting Unix timestamps in JavaScript&lt;/li&gt;
&lt;li&gt;Debugging an unknown timestamp&lt;/li&gt;
&lt;li&gt;A good rule for APIs&lt;/li&gt;
&lt;li&gt;Practical rules to remember&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is a Unix timestamp?
&lt;/h2&gt;

&lt;p&gt;A Unix timestamp represents the amount of time that has passed since January 1, 1970 at 00:00:00 UTC. This moment is commonly called the &lt;strong&gt;Unix epoch&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9do0q8ydbvotl7d9e6gx.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9do0q8ydbvotl7d9e6gx.webp" alt="Unix timestamp timeline from the 1970 Unix epoch to the Year 2038 limit" width="799" height="419"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For example, &lt;code&gt;0&lt;/code&gt; represents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1970-01-01 00:00:00 UTC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A timestamp such as &lt;code&gt;1790186400&lt;/code&gt; represents a specific moment after the Unix epoch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-09-23 18:00:00 UTC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is that the timestamp represents an &lt;strong&gt;instant in time&lt;/strong&gt;, not a formatted date. A value such as &lt;code&gt;2026-09-23 18:00:00&lt;/code&gt; is a human-readable representation. A Unix timestamp is simply a number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seconds vs milliseconds
&lt;/h2&gt;

&lt;p&gt;One of the most common timestamp bugs comes from confusing seconds with milliseconds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj83xe98jtd2jwrygpjez.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj83xe98jtd2jwrygpjez.webp" alt="Unix timestamp seconds vs milliseconds comparison with 10-digit and 13-digit examples" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Traditional Unix timestamps use seconds since January 1, 1970. Many programming environments, however, use milliseconds. The same instant looks like this in each unit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Seconds:      1790186400
Milliseconds: 1790186400000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Milliseconds usually have three extra digits. A quick rule of thumb:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10 digits -&amp;gt; usually seconds
13 digits -&amp;gt; usually milliseconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works for modern dates, but you should not rely on digit count as formal validation.&lt;/p&gt;

&lt;h2&gt;
  
  
  JavaScript uses milliseconds
&lt;/h2&gt;

&lt;p&gt;JavaScript's &lt;code&gt;Date&lt;/code&gt; API expects milliseconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1790186400000&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But many APIs return Unix timestamps in seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1790186400&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you pass that directly to &lt;code&gt;Date&lt;/code&gt;, JavaScript interprets it as milliseconds, and the resulting date lands in January 1970:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1790186400&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// 1970-01-21T17:16:26.400Z&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead, convert seconds to milliseconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1790186400&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reverse operation works the same way. To get the current Unix timestamp in seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To get milliseconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Does a Unix timestamp have a time zone?
&lt;/h2&gt;

&lt;p&gt;No. This is one of the most important things to understand about timestamps.&lt;/p&gt;

&lt;p&gt;A Unix timestamp does not contain a time zone. It represents one exact instant in time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Futc6jpj2yyth0cettjpl.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Futc6jpj2yyth0cettjpl.webp" alt="The same Unix timestamp displayed in Calgary, New York, London, and Tokyo time zones" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For example, &lt;code&gt;1790186400&lt;/code&gt; represents &lt;code&gt;2026-09-23 18:00:00 UTC&lt;/code&gt;. The exact same instant can be displayed as:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;th&gt;Local time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Calgary&lt;/td&gt;
&lt;td&gt;2026-09-23 12:00:00 MDT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New York&lt;/td&gt;
&lt;td&gt;2026-09-23 14:00:00 EDT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;London&lt;/td&gt;
&lt;td&gt;2026-09-23 19:00:00 BST&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The timestamp does not change. Only its formatted representation changes.&lt;/p&gt;

&lt;p&gt;Think of a Unix timestamp as a position on a global timeline. A time zone only changes how that position is displayed to a person.&lt;/p&gt;

&lt;h2&gt;
  
  
  UTC vs local time
&lt;/h2&gt;

&lt;p&gt;JavaScript makes this distinction visible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1790186400000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;toISOString()&lt;/code&gt; returns the date in UTC:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-09-23T18:00:00.000Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Z&lt;/code&gt; means UTC. But &lt;code&gt;toString()&lt;/code&gt; uses the computer's local time zone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A developer in Calgary might see something similar to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Wed Sep 23 2026 12:00:00 GMT-0600 (Mountain Daylight Time)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both values represent the exact same instant.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw76bbvqfqskckh73x885.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw76bbvqfqskckh73x885.webp" alt="Unix timestamp shown as different local times in multiple time zones with UTC conversion" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This difference is responsible for many apparent "timezone bugs". The timestamp is often correct — what changed is the way the application formatted it.&lt;/p&gt;

&lt;h2&gt;
  
  
  ISO 8601 and Unix timestamps are different things
&lt;/h2&gt;

&lt;p&gt;Developers sometimes mix up Unix timestamps and ISO 8601 dates.&lt;/p&gt;

&lt;p&gt;A Unix timestamp looks like &lt;code&gt;1790186400&lt;/code&gt;. An ISO 8601 date looks like &lt;code&gt;2026-09-23T18:00:00Z&lt;/code&gt;. They can both represent the same instant — the difference is representation.&lt;/p&gt;

&lt;p&gt;Unix timestamps are compact and convenient for machines. ISO 8601 strings are easier for humans to read and can explicitly contain a UTC offset. For example, these two strings represent the same moment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-09-23T12:00:00-06:00
2026-09-23T18:00:00Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common mistake #1: mixing seconds and milliseconds
&lt;/h2&gt;

&lt;p&gt;Consider an API response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1790186400&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You might write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createdAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is wrong if &lt;code&gt;created_at&lt;/code&gt; uses Unix seconds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbkd4gbjzwjs8dk7snbjj.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbkd4gbjzwjs8dk7snbjj.webp" alt="JavaScript Unix timestamp mistake caused by passing seconds instead of milliseconds to Date" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The correct version is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createdAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reverse mistake happens as well. Suppose an API expects seconds, and you write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expiresAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You send &lt;code&gt;1790186400000&lt;/code&gt;, but the server expects &lt;code&gt;1790186400&lt;/code&gt;. Read as seconds, your expiration date suddenly lands tens of thousands of years in the future.&lt;/p&gt;

&lt;p&gt;When working with timestamps, always check the API documentation for the expected unit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistake #2: assuming the timestamp is in local time
&lt;/h2&gt;

&lt;p&gt;A timestamp does not mean "12:00 Calgary time" or "14:00 New York time". It represents a global instant. The time zone matters only when converting that instant into a calendar date and clock time.&lt;/p&gt;

&lt;p&gt;This distinction becomes especially important when users are located in different countries. For example, your database might store &lt;code&gt;1790186400&lt;/code&gt;, and the frontend then displays that timestamp in each user's own time zone. That is usually much safer than storing separately formatted local dates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistake #3: removing the UTC indicator
&lt;/h2&gt;

&lt;p&gt;Consider this ISO string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-09-23T18:00:00Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Z&lt;/code&gt; tells the parser that the value is UTC. Compare it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-09-23T18:00:00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no time-zone information here. Depending on the language, library, environment or parsing rules, the second value may be interpreted differently — in JavaScript, for example, a date-time string without an offset is parsed as local time.&lt;/p&gt;

&lt;p&gt;When exchanging dates between systems, explicit time-zone information is much safer. Prefer formats such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-09-23T18:00:00Z
2026-09-23T12:00:00-06:00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;instead of an ambiguous local date.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistake #4: manually applying time-zone offsets
&lt;/h2&gt;

&lt;p&gt;Imagine you receive a timestamp from an API and convert it to a JavaScript &lt;code&gt;Date&lt;/code&gt;. JavaScript already knows how to display that date in the local time zone. A common mistake is then manually adding or subtracting an offset:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getHours&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In many cases this applies the time-zone conversion twice.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmmioom5f9yn516mameyi.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmmioom5f9yn516mameyi.webp" alt="Correct and incorrect ways to convert Unix timestamps between UTC and local time in JavaScript" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It also breaks when daylight saving time changes.&lt;/p&gt;

&lt;p&gt;Instead, keep the underlying timestamp unchanged and apply the desired time zone only when formatting the value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formatted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DateTimeFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en-CA&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;dateStyle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;timeStyle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;long&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;timeZone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;America/Edmonton&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formatted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is much safer than manually adding or subtracting hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistake #5: ignoring daylight saving time
&lt;/h2&gt;

&lt;p&gt;Time-zone offsets are not always constant. A location might use one UTC offset during part of the year and another during the rest of the year. Because of that, code such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;should not be used as a general way to convert UTC to a local time zone. The correct offset may depend on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;location;&lt;/li&gt;
&lt;li&gt;date;&lt;/li&gt;
&lt;li&gt;daylight saving rules;&lt;/li&gt;
&lt;li&gt;historical time-zone changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use an actual IANA time-zone identifier instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;America/Edmonton
America/New_York
Europe/London
Asia/Tokyo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Modern date/time libraries and &lt;code&gt;Intl.DateTimeFormat&lt;/code&gt; apply the appropriate rules for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistake #6: storing local time instead of an instant
&lt;/h2&gt;

&lt;p&gt;Imagine a database field contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-09-23 12:00:00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What does that mean? 12:00 where — Calgary, Toronto, London, UTC? Without additional information, the value is ambiguous.&lt;/p&gt;

&lt;p&gt;For events that represent an exact moment, storing either a Unix timestamp or an explicitly UTC-based datetime usually avoids this problem. Values such as &lt;code&gt;1790186400&lt;/code&gt; or &lt;code&gt;2026-09-23T18:00:00Z&lt;/code&gt; can be converted into the user's preferred local time later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not every date should be a Unix timestamp
&lt;/h2&gt;

&lt;p&gt;There is an important exception: some dates do not represent a global instant.&lt;/p&gt;

&lt;p&gt;A birthday is a good example. If someone was born on &lt;code&gt;1990-05-15&lt;/code&gt;, you care about the calendar date, not an exact UTC moment. The same applies to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;birthdays;&lt;/li&gt;
&lt;li&gt;anniversaries;&lt;/li&gt;
&lt;li&gt;all-day calendar events;&lt;/li&gt;
&lt;li&gt;billing dates;&lt;/li&gt;
&lt;li&gt;holidays.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Converting every date into a timestamp can introduce unnecessary time-zone problems — a birthday stored as midnight UTC can show up as the previous day for users west of Greenwich. Choose the representation based on what the value actually means.&lt;/p&gt;

&lt;h2&gt;
  
  
  Negative Unix timestamps
&lt;/h2&gt;

&lt;p&gt;Dates before January 1, 1970 are represented with negative timestamps. For example, &lt;code&gt;-1&lt;/code&gt; represents one second before the Unix epoch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1969-12-31 23:59:59 UTC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Many modern systems support negative Unix timestamps, but behavior can differ between platforms and older libraries. If your application works with historical dates, test them explicitly. The lower limit of 32-bit time is covered in Unix timestamp -2147483648 and the 1901 limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about the Year 2038 problem?
&lt;/h2&gt;

&lt;p&gt;A signed 32-bit integer can store values only up to &lt;code&gt;2147483647&lt;/code&gt;. Interpreted as seconds since the Unix epoch, that limit is reached on January 19, 2038 at 03:14:07 UTC. Older systems that store timestamps as signed 32-bit integers overflow after that point.&lt;/p&gt;

&lt;p&gt;Modern 64-bit systems do not have this practical limitation — a signed 64-bit timestamp in seconds covers a range far beyond anything normal applications need. However, the 2038 problem can still matter when working with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;legacy operating systems;&lt;/li&gt;
&lt;li&gt;embedded systems;&lt;/li&gt;
&lt;li&gt;old databases;&lt;/li&gt;
&lt;li&gt;binary protocols;&lt;/li&gt;
&lt;li&gt;older C applications.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Notable Unix timestamps
&lt;/h2&gt;

&lt;p&gt;A few values come up again and again in logs, tests and bug reports:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Timestamp&lt;/th&gt;
&lt;th&gt;Date (UTC)&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-2147483648&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1901-12-13 20:45:52&lt;/td&gt;
&lt;td&gt;Signed 32-bit minimum and 2038 overflow target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1970-01-01 00:00:00&lt;/td&gt;
&lt;td&gt;The Unix epoch, and why buggy dates show 1970&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1000000000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2001-09-09 01:46:40&lt;/td&gt;
&lt;td&gt;The billennium: timestamps become 10 digits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1234567890&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2009-02-13 23:31:30&lt;/td&gt;
&lt;td&gt;The famous sequential timestamp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1700000000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2023-11-14 22:13:20&lt;/td&gt;
&lt;td&gt;Round-number milestones from 1.5 to 2.1 billion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;2000000000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2033-05-18 03:33:20&lt;/td&gt;
&lt;td&gt;The last round billion before 2038&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;2147483647&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2038-01-19 03:14:07&lt;/td&gt;
&lt;td&gt;The Year 2038 problem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;4294967295&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2106-02-07 06:28:15&lt;/td&gt;
&lt;td&gt;The unsigned 32-bit limit and Year 2106&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Unix timestamp precision
&lt;/h2&gt;

&lt;p&gt;Unix-style timestamps can use different levels of precision. The same instant in each unit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Seconds:      1790186400
Milliseconds: 1790186400000
Microseconds: 1790186400000000
Nanoseconds:  1790186400000000000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is why receiving "a timestamp" from an API is not enough information. Before parsing it, check whether the value represents seconds, milliseconds, microseconds or nanoseconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Converting Unix timestamps in JavaScript
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Timestamp in seconds to Date
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1790186400&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Timestamp in milliseconds to Date
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1790186400000&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Current Unix timestamp in seconds
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Current Unix timestamp in milliseconds
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Date to Unix seconds
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Convert to UTC ISO format
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1790186400000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="c1"&gt;// 2026-09-23T18:00:00.000Z&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Debugging an unknown timestamp
&lt;/h2&gt;

&lt;p&gt;Suppose you find &lt;code&gt;1790186400&lt;/code&gt; in a log and do not know what it means. A useful debugging process is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Determine whether it is probably seconds, milliseconds, microseconds or nanoseconds.&lt;/li&gt;
&lt;li&gt;Convert it into UTC.&lt;/li&gt;
&lt;li&gt;Compare the result with the expected date.&lt;/li&gt;
&lt;li&gt;Check whether the system displays the result in local time.&lt;/li&gt;
&lt;li&gt;Look for any manually applied time-zone offsets.&lt;/li&gt;
&lt;li&gt;Verify how the originating API or database documents the field.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For quick debugging, use the &lt;a href="https://helpers.work/tool/timestamp" rel="noopener noreferrer"&gt;Unix Timestamp Converter&lt;/a&gt; to convert between Unix timestamps and human-readable dates in any time zone.&lt;/p&gt;

&lt;h2&gt;
  
  
  A good rule for APIs
&lt;/h2&gt;

&lt;p&gt;When designing APIs, consistency matters more than which representation you choose. You might decide that your API always returns ISO 8601 strings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-23T18:00:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or you might use Unix timestamps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1790186400&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both approaches work. Problems begin when one endpoint returns seconds (&lt;code&gt;1790186400&lt;/code&gt;), another returns milliseconds (&lt;code&gt;1790186400000&lt;/code&gt;), and a third returns a local datetime (&lt;code&gt;2026-09-23 12:00:00&lt;/code&gt;) without documenting the difference.&lt;/p&gt;

&lt;p&gt;Pick a convention and use it consistently across your API. The same applies to tokens: the &lt;code&gt;exp&lt;/code&gt;, &lt;code&gt;iat&lt;/code&gt; and &lt;code&gt;nbf&lt;/code&gt; claims in a JWT are always Unix seconds, which you can check with the &lt;a href="https://helpers.work/tool/jwt-decoder" rel="noopener noreferrer"&gt;JWT Decoder&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical rules to remember
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A Unix timestamp represents an instant in time.&lt;/li&gt;
&lt;li&gt;Unix timestamps do not contain a time zone.&lt;/li&gt;
&lt;li&gt;Traditional Unix timestamps use seconds.&lt;/li&gt;
&lt;li&gt;JavaScript timestamps normally use milliseconds.&lt;/li&gt;
&lt;li&gt;Do not apply fixed UTC offsets manually when you can use a real time-zone database.&lt;/li&gt;
&lt;li&gt;Store exact moments in a time-zone-independent format.&lt;/li&gt;
&lt;li&gt;Convert to the user's local time only when displaying the value.&lt;/li&gt;
&lt;li&gt;Use explicit time-zone information when exchanging formatted datetime strings.&lt;/li&gt;
&lt;li&gt;Always verify the timestamp unit used by an API.&lt;/li&gt;
&lt;li&gt;Do not use timestamps for calendar-only dates unless you actually need an instant in time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Unix timestamps are simple numbers, but the systems around them — time zones, daylight saving time, date formatting and different precision levels — make them surprisingly easy to misuse.&lt;/p&gt;

&lt;p&gt;The most useful mental model is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A Unix timestamp identifies a point on the global timeline. A time zone only determines how that point is displayed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once you separate those two ideas, most timestamp problems become much easier to understand and debug.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://helpers.work/blog/unix-timestamps" rel="noopener noreferrer"&gt;helpers.work&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
