<?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: Arbaz</title>
    <description>The latest articles on DEV Community by Arbaz (@arbaz_mr).</description>
    <link>https://dev.to/arbaz_mr</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%2F4188476%2Fcc6b1d95-6749-4e3b-84e7-9e3728ce24c9.png</url>
      <title>DEV Community: Arbaz</title>
      <link>https://dev.to/arbaz_mr</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/arbaz_mr"/>
    <language>en</language>
    <item>
      <title>Building an Equipment Checkout System with PostgreSQL, Node.js, and Express</title>
      <dc:creator>Arbaz</dc:creator>
      <pubDate>Sat, 17 Oct 2026 12:40:47 +0000</pubDate>
      <link>https://dev.to/arbaz_mr/building-an-equipment-checkout-system-with-postgresql-nodejs-and-express-hp</link>
      <guid>https://dev.to/arbaz_mr/building-an-equipment-checkout-system-with-postgresql-nodejs-and-express-hp</guid>
      <description>&lt;p&gt;&lt;em&gt;Database design, REST APIs, concurrency control, and practical equipment custody workflows.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Imagine an IT department managing laptops, monitors, and tablets for 200 employees.&lt;/p&gt;

&lt;p&gt;Every time a device is issued, someone records the assignment. Every time it is returned, someone updates the equipment register.&lt;/p&gt;

&lt;p&gt;That sounds like a simple CRUD application.&lt;/p&gt;

&lt;p&gt;But consider what happens when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two administrators try to assign the same laptop simultaneously.&lt;/li&gt;
&lt;li&gt;Someone returns equipment that was already marked as returned.&lt;/li&gt;
&lt;li&gt;An employee leaves, but their assigned devices are difficult to identify.&lt;/li&gt;
&lt;li&gt;A transaction updates the asset status but fails before saving the checkout record.&lt;/li&gt;
&lt;li&gt;A device changes hands without preserving its previous assignment history.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren't just interface problems. They are &lt;strong&gt;data integrity and concurrency problems&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In this tutorial, we'll build the core of an equipment checkout system using PostgreSQL, Node.js, and Express.&lt;/p&gt;

&lt;p&gt;By the end, you'll understand how to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Design a relational model for physical assets and employee custody.&lt;/li&gt;
&lt;li&gt;Prevent duplicate active equipment assignments.&lt;/li&gt;
&lt;li&gt;Build transactional checkout and return endpoints.&lt;/li&gt;
&lt;li&gt;Protect shared assets from concurrent updates.&lt;/li&gt;
&lt;li&gt;Record custody events.&lt;/li&gt;
&lt;li&gt;Test common failure scenarios.&lt;/li&gt;
&lt;li&gt;Identify what must be added before production deployment.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is a small, executable educational example—not a complete enterprise asset management platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Understand the Business Problem
&lt;/h2&gt;

&lt;p&gt;An equipment checkout system needs to distinguish between an &lt;strong&gt;asset&lt;/strong&gt; and an &lt;strong&gt;assignment&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;An asset is a physical item owned or managed by an organization.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Asset ID&lt;/th&gt;
&lt;th&gt;Equipment&lt;/th&gt;
&lt;th&gt;Current status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LAP-101&lt;/td&gt;
&lt;td&gt;Dell Latitude laptop&lt;/td&gt;
&lt;td&gt;Available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LAP-102&lt;/td&gt;
&lt;td&gt;MacBook Pro&lt;/td&gt;
&lt;td&gt;Checked out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MON-201&lt;/td&gt;
&lt;td&gt;Dell monitor&lt;/td&gt;
&lt;td&gt;Maintenance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;An assignment describes a period during which a particular employee is responsible for an asset.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Asset&lt;/th&gt;
&lt;th&gt;Employee&lt;/th&gt;
&lt;th&gt;Issued&lt;/th&gt;
&lt;th&gt;Returned&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LAP-101&lt;/td&gt;
&lt;td&gt;Alice&lt;/td&gt;
&lt;td&gt;Oct 1&lt;/td&gt;
&lt;td&gt;Oct 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LAP-101&lt;/td&gt;
&lt;td&gt;Bob&lt;/td&gt;
&lt;td&gt;Oct 12&lt;/td&gt;
&lt;td&gt;Not yet returned&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Notice that the same laptop can have multiple historical assignments but should have only &lt;strong&gt;one active checkout&lt;/strong&gt; at a time.&lt;/p&gt;

&lt;p&gt;That's our most important business rule.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core workflow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Register asset
     |
     v
  Available
     |
     | Checkout
     v
 Checked out
     |
     | Return
     v
  Available
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Additional states such as maintenance, reservation, retirement, and transfer can be introduced later.&lt;/p&gt;

&lt;p&gt;For this tutorial, we'll focus on the checkout/return lifecycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Define the Database Invariants
&lt;/h2&gt;

&lt;p&gt;Before designing database tables, establish conditions the application must preserve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 1:&lt;/strong&gt; Each asset has a unique asset tag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 2:&lt;/strong&gt; An asset cannot have multiple active checkout records.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 3:&lt;/strong&gt; Only available assets may be checked out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 4:&lt;/strong&gt; A checkout must reference a valid, active employee.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 5:&lt;/strong&gt; A return must close the active checkout record.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 6:&lt;/strong&gt; Asset status and checkout history must be updated together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 7:&lt;/strong&gt; Historical custody records should be retained.&lt;/p&gt;

&lt;p&gt;Some of these rules can be enforced directly by PostgreSQL constraints. Others require transactions and application-level validation.&lt;/p&gt;

&lt;p&gt;Using both layers makes the system more reliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Create the PostgreSQL Schema
&lt;/h2&gt;

&lt;p&gt;We'll use three primary tables and one simple event table.&lt;/p&gt;

&lt;p&gt;The examples use standard PostgreSQL features.&lt;/p&gt;

&lt;h3&gt;
  
  
  Employees
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;full_name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;is_active&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An employee can become inactive without deleting their historical checkout records.&lt;/p&gt;

&lt;h3&gt;
  
  
  Assets
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;assets&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;asset_tag&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;serial_number&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'available'&lt;/span&gt;
        &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'available'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'checked_out'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'maintenance'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'retired'&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;asset_tag&lt;/code&gt; uniquely identifies a physical device.&lt;/p&gt;

&lt;p&gt;A QR code or barcode can encode that identifier or a URL that resolves to its record.&lt;/p&gt;

&lt;p&gt;Importantly, &lt;strong&gt;scanning a QR code does not automatically establish custody or continuous location tracking&lt;/strong&gt;. The business application must explicitly record those activities.&lt;/p&gt;

&lt;h3&gt;
  
  
  Checkout history
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;checkouts&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="n"&gt;asset_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
        &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

    &lt;span class="n"&gt;employee_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
        &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

    &lt;span class="n"&gt;checked_out_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
        &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;

    &lt;span class="n"&gt;expected_return_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;returned_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="n"&gt;issued_condition&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;return_condition&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;expected_return_at&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
        &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;expected_return_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;checked_out_at&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;

    &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;returned_at&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
        &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;returned_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;checked_out_at&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We keep historical checkouts instead of deleting them.&lt;/p&gt;

&lt;p&gt;An active checkout is defined as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;returned_at&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Prevent duplicate active checkouts
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;one_active_checkout_per_asset&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;checkouts&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;asset_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;returned_at&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a PostgreSQL &lt;strong&gt;partial unique index&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It enforces a critical invariant at the database level: no more than one active checkout record per asset.&lt;/p&gt;

&lt;p&gt;Unlike a simple application-level &lt;code&gt;SELECT&lt;/code&gt; check, the unique index still protects data when concurrent requests attempt to insert conflicting records.&lt;/p&gt;

&lt;p&gt;PostgreSQL documents partial indexes here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.postgresql.org/docs/current/indexes-partial.html" rel="noopener noreferrer"&gt;https://www.postgresql.org/docs/current/indexes-partial.html&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Record custody events
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;asset_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="n"&gt;asset_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
        &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

    &lt;span class="n"&gt;checkout_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;
        &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;checkouts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

    &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
        &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'CHECKED_OUT'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'RETURNED'&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;

    &lt;span class="n"&gt;occurred_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
        &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This small event table provides an operational history of checkout and return events.&lt;/p&gt;

&lt;p&gt;It is not a full security audit log because it does not yet identify the authenticated administrator who performed the action.&lt;/p&gt;

&lt;p&gt;A production audit log would typically include a verified actor identity and additional event context.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Insert Example Data
&lt;/h2&gt;

&lt;p&gt;Let's create two employees and two assets.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Alice Carter'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'alice@example.com'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Bob Smith'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'bob@example.com'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;assets&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;asset_tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'LAP-101'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Dell Latitude'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Laptop'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'MON-201'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Dell Monitor'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Monitor'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are fictional example records.&lt;/p&gt;

&lt;p&gt;Both devices initially have the status &lt;code&gt;available&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Set Up Node.js and Express
&lt;/h2&gt;

&lt;p&gt;Start a new Node.js project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;equipment-checkout-api
&lt;span class="nb"&gt;cd &lt;/span&gt;equipment-checkout-api
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;express pg dotenv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;"type": "module"&lt;/code&gt; to &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Create a PostgreSQL database, then execute the schema and seed statements above using a PostgreSQL client such as &lt;code&gt;psql&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Create a &lt;code&gt;.env&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DATABASE_URL=postgresql://localhost:5432/asset_tracking
PORT=3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the connection string with one that matches your local PostgreSQL configuration.&lt;/p&gt;

&lt;p&gt;Never commit credentials or &lt;code&gt;.env&lt;/code&gt; files containing secrets to a public repository.&lt;/p&gt;

&lt;h3&gt;
  
  
  Database connection
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;db.js&lt;/code&gt;:&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;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dotenv/config&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;pg&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pool&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;pg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We use a connection pool for database operations.&lt;/p&gt;

&lt;p&gt;However, &lt;strong&gt;every statement within a transaction must execute using the same checked-out client&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The node-postgres documentation explicitly requires this.&lt;/p&gt;

&lt;p&gt;Reference:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://node-postgres.com/features/transactions" rel="noopener noreferrer"&gt;https://node-postgres.com/features/transactions&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Build the Checkout API
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;server.js&lt;/code&gt;.&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;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./db.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/checkouts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;assetId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;employeeId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isSafeInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;assetId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;assetId&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isSafeInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;employeeId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;employeeId&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Valid assetId and employeeId are required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BEGIN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;assetResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`SELECT id, status
       FROM assets
       WHERE id = $1
       FOR UPDATE`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;assetId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;asset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;assetResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ROLLBACK&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Asset not found&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;available&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ROLLBACK&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;409&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Asset is not available&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;employeeResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`SELECT id
       FROM employees
       WHERE id = $1
         AND is_active = TRUE`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;employeeId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;employeeResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rowCount&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ROLLBACK&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Employee not found or inactive&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;checkoutResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`INSERT INTO checkouts (
         asset_id,
         employee_id
       )
       VALUES ($1, $2)
       RETURNING id, asset_id, employee_id,
                 checked_out_at`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;assetId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;employeeId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;checkout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;checkoutResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`UPDATE assets
       SET status = 'checked_out'
       WHERE id = $1`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;assetId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`INSERT INTO asset_events (
         asset_id,
         checkout_id,
         event_type
       )
       VALUES ($1, $2, 'CHECKED_OUT')`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;assetId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;COMMIT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Equipment checked out&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ROLLBACK&lt;/span&gt;&lt;span class="dl"&gt;"&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;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Checkout error:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;23505&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;409&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Conflicting active assignment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Checkout failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why use a transaction?
&lt;/h3&gt;

&lt;p&gt;A successful checkout changes several records:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The checkout history gains a new active assignment.&lt;/li&gt;
&lt;li&gt;The asset status becomes &lt;code&gt;checked_out&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;An event is recorded.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These changes must succeed together.&lt;/p&gt;

&lt;p&gt;If any statement fails, the transaction rolls back.&lt;/p&gt;

&lt;p&gt;This prevents a partially completed checkout from leaving the records inconsistent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why use &lt;code&gt;FOR UPDATE&lt;/code&gt;?
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;assets&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The row lock prevents another conflicting transaction from updating or locking the same asset row until the current transaction finishes.&lt;/p&gt;

&lt;p&gt;Consider two checkout requests arriving at nearly the same time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request A locks LAP-101

Request B waits for LAP-101

Request A creates assignment
Request A commits

Request B reads the updated status

Request B receives HTTP 409
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In combination with the partial unique index, this provides strong protection against duplicate active assignments.&lt;/p&gt;

&lt;p&gt;The example assumes every custody-modifying operation follows the same transaction and locking conventions.&lt;/p&gt;

&lt;p&gt;Reference:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.postgresql.org/docs/current/explicit-locking.html" rel="noopener noreferrer"&gt;https://www.postgresql.org/docs/current/explicit-locking.html&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Build the Return API
&lt;/h2&gt;

&lt;p&gt;Returning equipment must close its active assignment and update the corresponding asset status.&lt;/p&gt;

&lt;p&gt;To reduce lock-order conflicts, we'll locate the checkout's asset, lock the asset first, and then lock the checkout record.&lt;/p&gt;

&lt;p&gt;Add this endpoint to &lt;code&gt;server.js&lt;/code&gt;:&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;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/checkouts/:id/return&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;checkoutId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isSafeInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;checkoutId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;checkoutId&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid checkout ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BEGIN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lookup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`SELECT asset_id
       FROM checkouts
       WHERE id = $1`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;checkoutId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rowCount&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ROLLBACK&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Checkout not found&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;assetId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;asset_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`SELECT id
       FROM assets
       WHERE id = $1
       FOR UPDATE`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;assetId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;checkoutResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`SELECT id, returned_at
       FROM checkouts
       WHERE id = $1
       FOR UPDATE`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;checkoutId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;checkoutResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rowCount&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="nx"&gt;checkoutResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;returned_at&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ROLLBACK&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;409&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Checkout is not active&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`UPDATE checkouts
       SET returned_at = NOW()
       WHERE id = $1
       RETURNING id, asset_id, employee_id,
                 returned_at`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;checkoutId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`UPDATE assets
       SET status = 'available'
       WHERE id = $1`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;assetId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`INSERT INTO asset_events (
         asset_id,
         checkout_id,
         event_type
       )
       VALUES ($1, $2, 'RETURNED')`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;assetId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;checkoutId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;COMMIT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Equipment returned&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ROLLBACK&lt;/span&gt;&lt;span class="dl"&gt;"&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;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Return error:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Return failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a deliberately simple return workflow.&lt;/p&gt;

&lt;p&gt;It assumes returned equipment is available for reassignment.&lt;/p&gt;

&lt;p&gt;In a real IT department, damaged equipment may need inspection or maintenance before becoming available again.&lt;/p&gt;

&lt;p&gt;For that reason, a production system should support condition checks and an appropriate post-return status.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Add an Employee Equipment Lookup API
&lt;/h2&gt;

&lt;p&gt;During offboarding, IT needs to identify every device currently assigned to an employee.&lt;/p&gt;

&lt;p&gt;Add the following endpoint:&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;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/employees/:id/assets&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;employeeId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isSafeInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;employeeId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;employeeId&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid employee ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;try&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`SELECT
         a.asset_tag,
         a.name,
         a.category,
         c.id AS checkout_id,
         c.checked_out_at
       FROM checkouts c
       JOIN assets a ON a.id = c.asset_id
       WHERE c.employee_id = $1
         AND c.returned_at IS NULL
       ORDER BY c.checked_out_at DESC`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;employeeId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;employeeId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&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;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Asset lookup error:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unable to fetch assets&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This endpoint queries the active checkout records rather than only checking the asset status.&lt;/p&gt;

&lt;p&gt;For employee offboarding, that's the relevant source of custody information.&lt;/p&gt;

&lt;h3&gt;
  
  
  Start the server
&lt;/h3&gt;

&lt;p&gt;Finally, add:&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;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="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="s2"&gt;`Server running on port &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node server.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  9. Test the Workflow
&lt;/h2&gt;

&lt;p&gt;Let's check out the first laptop to the first employee.&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;-X&lt;/span&gt; POST http://localhost:3000/api/checkouts &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"assetId":1,"employeeId":1}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the seed data was inserted into a new database in the stated order, this should create an active checkout.&lt;/p&gt;

&lt;p&gt;The server should return HTTP &lt;code&gt;201&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now repeat the same request.&lt;/p&gt;

&lt;p&gt;Because the asset is already checked out, the second request should return HTTP &lt;code&gt;409&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To retrieve the employee's equipment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:3000/api/employees/1/assets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To return the equipment, use the checkout ID from the first successful checkout response:&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;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  http://localhost:3000/api/checkouts/1/return
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The example above assumes the first generated checkout ID is &lt;code&gt;1&lt;/code&gt;; use the actual returned ID in your environment.&lt;/p&gt;

&lt;p&gt;After the return, the asset should be available for another checkout.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test concurrent requests
&lt;/h3&gt;

&lt;p&gt;The most important concurrency test is to submit two checkout requests for the same available asset at approximately the same time.&lt;/p&gt;

&lt;p&gt;Expected behavior:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One request succeeds.&lt;/li&gt;
&lt;li&gt;The other receives a conflict.&lt;/li&gt;
&lt;li&gt;Only one active checkout exists in the database.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Verify that final condition directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;checkouts&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;asset_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;returned_at&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The query should return no more than one row.&lt;/p&gt;

&lt;p&gt;The partial unique index provides the database-level guarantee for that condition.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. What This Example Does—and Doesn't—Guarantee
&lt;/h2&gt;

&lt;p&gt;The example implements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unique asset identifiers.&lt;/li&gt;
&lt;li&gt;Historical checkout records.&lt;/li&gt;
&lt;li&gt;Prevention of duplicate active checkouts.&lt;/li&gt;
&lt;li&gt;Transactional checkout and return operations.&lt;/li&gt;
&lt;li&gt;Row locking for asset-level concurrency.&lt;/li&gt;
&lt;li&gt;Basic checkout and return events.&lt;/li&gt;
&lt;li&gt;Employee equipment lookup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt; implement a complete production system.&lt;/p&gt;

&lt;p&gt;Before deployment, several additional components are necessary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Authentication and authorization
&lt;/h3&gt;

&lt;p&gt;Each request must identify its authenticated user.&lt;/p&gt;

&lt;p&gt;An employee should not be able to check out an arbitrary device or retrieve another employee's records without authorization.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tenant isolation
&lt;/h3&gt;

&lt;p&gt;If multiple organizations use the same application, their assets, employees, and checkout histories must be isolated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Idempotency
&lt;/h3&gt;

&lt;p&gt;A client may retry a request after a network timeout even if the original operation succeeded.&lt;/p&gt;

&lt;p&gt;Production APIs should consider persistent idempotency keys to identify repeated requests safely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Employee transfers
&lt;/h3&gt;

&lt;p&gt;Transferring equipment should preserve its previous custody period while creating a new assignment.&lt;/p&gt;

&lt;p&gt;A transfer workflow should be handled transactionally, with explicit business rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Full audit history
&lt;/h3&gt;

&lt;p&gt;A production audit trail should record the authenticated actor, relevant changes, timestamps, and appropriate context.&lt;/p&gt;

&lt;h3&gt;
  
  
  Operational controls
&lt;/h3&gt;

&lt;p&gt;Real applications also need structured logging, database migrations, monitoring, secure configuration, backup and recovery procedures, and automated integration tests.&lt;/p&gt;

&lt;p&gt;A particularly important engineering consideration is that the checkout and return endpoints rely on consistent asset-first locking. Any future transfer or maintenance workflow must coordinate with that strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Why These Design Decisions Matter to IT Teams
&lt;/h2&gt;

&lt;p&gt;From a business perspective, these database concepts solve practical problems.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Technical decision&lt;/th&gt;
&lt;th&gt;Operational benefit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unique asset tags&lt;/td&gt;
&lt;td&gt;Consistent equipment identification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Partial unique index&lt;/td&gt;
&lt;td&gt;Prevents conflicting active assignments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Historical checkouts&lt;/td&gt;
&lt;td&gt;Preserves previous custody records&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transactions&lt;/td&gt;
&lt;td&gt;Prevents partially completed handovers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Row locking&lt;/td&gt;
&lt;td&gt;Protects concurrent equipment assignments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Employee lookup API&lt;/td&gt;
&lt;td&gt;Supports offboarding and reconciliation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custody events&lt;/td&gt;
&lt;td&gt;Provides a foundation for change history&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For growing businesses, the most useful equipment management system is not necessarily the one with the longest feature list.&lt;/p&gt;

&lt;p&gt;It is one that keeps responsibility and asset records consistent as equipment moves between employees and locations.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. How This Relates to Real Asset Management Platforms
&lt;/h2&gt;

&lt;p&gt;Building an internal equipment checkout system can be a valuable engineering project.&lt;/p&gt;

&lt;p&gt;However, teams also need to consider ongoing maintenance, authorization, auditing, user experience, and the operational effort required to manage equipment at scale.&lt;/p&gt;

&lt;p&gt;That is why some organizations evaluate dedicated physical asset management platforms instead of maintaining custom software.&lt;/p&gt;

&lt;p&gt;For example, &lt;a href="https://www.stackroom.io" rel="noopener noreferrer"&gt;Stackroom&lt;/a&gt; focuses on centralized physical asset records, equipment assignments, documented handovers and returns, and audit workflows.&lt;/p&gt;

&lt;p&gt;For an operational explanation of the equipment handover process, see:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.stackroom.io/knowledge-base/equipment-checkout-process/" rel="noopener noreferrer"&gt;Equipment Checkout Process Guide — Stackroom&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I am associated with Stackroom, a physical asset and equipment management platform.&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;A reliable equipment checkout system is more than an inventory database.&lt;/p&gt;

&lt;p&gt;It needs to preserve custody information, enforce business rules, and remain consistent when operations happen concurrently.&lt;/p&gt;

&lt;p&gt;The most important lessons are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Model assets and checkout periods separately.&lt;/li&gt;
&lt;li&gt;Use database constraints for critical invariants.&lt;/li&gt;
&lt;li&gt;Wrap multi-step state changes in transactions.&lt;/li&gt;
&lt;li&gt;Lock shared asset records consistently.&lt;/li&gt;
&lt;li&gt;Retain historical checkout information.&lt;/li&gt;
&lt;li&gt;Record custody events.&lt;/li&gt;
&lt;li&gt;Test duplicate requests, concurrency, and failure conditions.&lt;/li&gt;
&lt;li&gt;Treat authorization and tenant isolation as production requirements.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;A good equipment checkout system should answer not only “What equipment do we own?” but also “Who currently has it, and how did that assignment happen?”&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Discussion
&lt;/h3&gt;

&lt;p&gt;If you were designing an equipment tracking application, would you store transfers as separate checkout periods or introduce a dedicated transfer entity?&lt;/p&gt;

&lt;p&gt;What trade-offs would influence your decision?&lt;/p&gt;




&lt;h3&gt;
  
  
  Technical references
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/current/indexes-partial.html" rel="noopener noreferrer"&gt;PostgreSQL — Partial Indexes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/current/explicit-locking.html" rel="noopener noreferrer"&gt;PostgreSQL — Explicit Locking&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://node-postgres.com/features/transactions" rel="noopener noreferrer"&gt;node-postgres — Transactions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://expressjs.com/en/5x/guide/error-handling/" rel="noopener noreferrer"&gt;Express — Error Handling&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>backend</category>
      <category>postgres</category>
      <category>architecture</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why Your Asset Tracking System Needs an Audit Trail (Not Just a Database)</title>
      <dc:creator>Arbaz</dc:creator>
      <pubDate>Fri, 16 Oct 2026 11:41:56 +0000</pubDate>
      <link>https://dev.to/arbaz_mr/why-your-asset-tracking-system-needs-an-audit-trail-not-just-a-database-3864</link>
      <guid>https://dev.to/arbaz_mr/why-your-asset-tracking-system-needs-an-audit-trail-not-just-a-database-3864</guid>
      <description>&lt;p&gt;*&lt;em&gt;Your database can tell you who has a laptop today. But can it explain how that laptop got there?&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
Imagine you're building an asset tracking system for a company with 500 employees.&lt;/p&gt;

&lt;p&gt;Your application manages laptops, monitors, smartphones, and other company equipment.&lt;/p&gt;

&lt;p&gt;Everything looks fine.&lt;/p&gt;

&lt;p&gt;You have a database, an asset dashboard, and an API to assign equipment to employees.&lt;/p&gt;

&lt;p&gt;Then one day, an IT administrator notices something strange.&lt;/p&gt;

&lt;p&gt;A laptop originally assigned to Sarah now appears under John's name.&lt;/p&gt;

&lt;p&gt;Sarah says she returned it two weeks ago.&lt;/p&gt;

&lt;p&gt;John says he received it yesterday.&lt;/p&gt;

&lt;p&gt;The database shows:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Asset&lt;/strong&gt;: LAP-1042&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Assigned&lt;/strong&gt; To: John&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Status&lt;/strong&gt;: Assigned&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Location&lt;/strong&gt;: London Office&lt;/p&gt;

&lt;p&gt;Technically, the data is correct.&lt;/p&gt;

&lt;p&gt;But there's a problem.&lt;/p&gt;

&lt;p&gt;You cannot answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who transferred the laptop?&lt;/li&gt;
&lt;li&gt;When did the assignment change?&lt;/li&gt;
&lt;li&gt;Was it returned to IT first?&lt;/li&gt;
&lt;li&gt;Who updated the record?&lt;/li&gt;
&lt;li&gt;Was the transfer authorized?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your database tells you the current state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It doesn't necessarily tell you the story behind that state.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is why an asset management audit trail matters.&lt;/p&gt;

&lt;p&gt;And it's something developers should think about before their application reaches production.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. A Database Stores State. An Audit Trail Explains Change.
&lt;/h2&gt;

&lt;p&gt;Most developers begin an asset management application with a straightforward database table.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1i68d6n7rtzc7p801kxm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1i68d6n7rtzc7p801kxm.png" alt=" " width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This table is useful.&lt;/p&gt;

&lt;p&gt;It answers questions about the current inventory.&lt;/p&gt;

&lt;p&gt;But suppose Sarah transfers a laptop to John.&lt;/p&gt;

&lt;p&gt;Your backend updates the assignment:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;UPDATE assets SET assigned_to = 'John' WHERE id = 1042;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The previous value disappears from the current-state table.&lt;/p&gt;

&lt;p&gt;Unless you have another mechanism for recording changes, the application no longer knows that Sarah previously held the laptop.&lt;/p&gt;

&lt;p&gt;Now imagine this happening hundreds of times across multiple departments.&lt;/p&gt;

&lt;p&gt;Assets get reassigned, returned, repaired, moved, and retired.&lt;/p&gt;

&lt;p&gt;Every update modifies the current state.&lt;/p&gt;

&lt;p&gt;Without a separate change history, reconstructing what happened becomes difficult.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An audit trail solves this problem by recording important events alongside the current asset record.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  2. What Exactly Is an Asset Management Audit Trail?
&lt;/h2&gt;

&lt;p&gt;An audit trail is a chronological record of actions or changes affecting an asset.&lt;/p&gt;

&lt;p&gt;Instead of only storing the latest assignment, it records the significant events that led to the current state.&lt;/p&gt;

&lt;p&gt;For example:&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;Event&lt;/th&gt;
&lt;th&gt;Actor&lt;/th&gt;
&lt;th&gt;Details&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Oct 1, 09:00&lt;/td&gt;
&lt;td&gt;Asset created&lt;/td&gt;
&lt;td&gt;IT Admin&lt;/td&gt;
&lt;td&gt;Laptop LAP-1042 added&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Oct 2, 10:30&lt;/td&gt;
&lt;td&gt;Assigned&lt;/td&gt;
&lt;td&gt;IT Admin&lt;/td&gt;
&lt;td&gt;Assigned to Sarah&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Oct 10, 14:15&lt;/td&gt;
&lt;td&gt;Returned&lt;/td&gt;
&lt;td&gt;IT Admin&lt;/td&gt;
&lt;td&gt;Returned by Sarah&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Oct 11, 11:00&lt;/td&gt;
&lt;td&gt;Assigned&lt;/td&gt;
&lt;td&gt;IT Admin&lt;/td&gt;
&lt;td&gt;Assigned to John&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Now the system can answer more useful questions.&lt;/p&gt;

&lt;p&gt;Who previously held the laptop?&lt;/p&gt;

&lt;p&gt;When was it returned?&lt;/p&gt;

&lt;p&gt;Who recorded the new assignment?&lt;/p&gt;

&lt;p&gt;Was the asset available between transfers?&lt;/p&gt;

&lt;p&gt;This becomes valuable for physical asset management, financial systems, administrative applications, and many other business workflows.&lt;/p&gt;

&lt;p&gt;An audit trail is not just about security.&lt;/p&gt;

&lt;p&gt;It's also about understanding the lifecycle of important business data.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Audit Logs vs Application Logs: What's the Difference?
&lt;/h2&gt;

&lt;p&gt;Developers sometimes assume normal application logging is enough.&lt;/p&gt;

&lt;p&gt;For example, your backend might produce:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;INFO: Asset assignment updated successfully&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This helps developers understand that an operation ran.&lt;/p&gt;

&lt;p&gt;But it doesn't necessarily explain which equipment was affected, what changed, or who initiated the action.&lt;/p&gt;

&lt;p&gt;Application logs and audit logs serve different purposes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Application logs&lt;/strong&gt; primarily help engineers monitor application behavior and troubleshoot technical issues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audit logs&lt;/strong&gt; record business-relevant actions that may need to be reviewed later.&lt;/p&gt;

&lt;p&gt;Consider an employee equipment transfer.&lt;/p&gt;

&lt;p&gt;An application log might say:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PUT /assets/1042 → 200 OK&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;An audit event should provide more context:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Asset ID: 1042&lt;/li&gt;
&lt;li&gt;Action: Assignment changed&lt;/li&gt;
&lt;li&gt;Previous custodian: Sarah&lt;/li&gt;
&lt;li&gt;New custodian: John&lt;/li&gt;
&lt;li&gt;Performed by: IT Admin&lt;/li&gt;
&lt;li&gt;Timestamp: 2026-10-11T11:00:00Z&lt;/li&gt;
&lt;li&gt;Reason: Department transfer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The application log tells you that an API request succeeded.&lt;/p&gt;

&lt;p&gt;The audit trail explains what happened to the business record.&lt;/p&gt;

&lt;p&gt;Both are useful, but they are not interchangeable.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. Designing an Audit Trail Database
&lt;/h2&gt;

&lt;p&gt;Let's look at a simple design.&lt;/p&gt;

&lt;p&gt;You already have an &lt;code&gt;assets&lt;/code&gt; table containing the current state.&lt;/p&gt;

&lt;p&gt;Now introduce an &lt;code&gt;asset_audit_logs&lt;/code&gt; table.&lt;/p&gt;

&lt;p&gt;A possible PostgreSQL schema is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;assets&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;id&lt;/td&gt;
&lt;td&gt;UUID&lt;/td&gt;
&lt;td&gt;Unique asset identifier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;name&lt;/td&gt;
&lt;td&gt;TEXT&lt;/td&gt;
&lt;td&gt;Asset name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;assigned_to&lt;/td&gt;
&lt;td&gt;UUID&lt;/td&gt;
&lt;td&gt;Current custodian&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;status&lt;/td&gt;
&lt;td&gt;TEXT&lt;/td&gt;
&lt;td&gt;Current asset status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;updated_at&lt;/td&gt;
&lt;td&gt;TIMESTAMPTZ&lt;/td&gt;
&lt;td&gt;Last update time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;asset_audit_logs&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;id&lt;/td&gt;
&lt;td&gt;UUID&lt;/td&gt;
&lt;td&gt;Unique event identifier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;asset_id&lt;/td&gt;
&lt;td&gt;UUID&lt;/td&gt;
&lt;td&gt;Related asset&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;action&lt;/td&gt;
&lt;td&gt;TEXT&lt;/td&gt;
&lt;td&gt;Type of action&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;actor_id&lt;/td&gt;
&lt;td&gt;UUID&lt;/td&gt;
&lt;td&gt;User who performed it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;previous_value&lt;/td&gt;
&lt;td&gt;JSONB&lt;/td&gt;
&lt;td&gt;Previous relevant state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;new_value&lt;/td&gt;
&lt;td&gt;JSONB&lt;/td&gt;
&lt;td&gt;Updated relevant state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reason&lt;/td&gt;
&lt;td&gt;TEXT&lt;/td&gt;
&lt;td&gt;Optional explanation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;created_at&lt;/td&gt;
&lt;td&gt;TIMESTAMPTZ&lt;/td&gt;
&lt;td&gt;Event timestamp&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The audit table should record changes such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Asset creation&lt;/li&gt;
&lt;li&gt;Assignment&lt;/li&gt;
&lt;li&gt;Return&lt;/li&gt;
&lt;li&gt;Custodian transfer&lt;/li&gt;
&lt;li&gt;Location update&lt;/li&gt;
&lt;li&gt;Maintenance status change&lt;/li&gt;
&lt;li&gt;Asset retirement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not every mouse click needs an audit event.&lt;/p&gt;

&lt;p&gt;Focus on actions that affect accountability, operational decisions, or important asset records.&lt;/p&gt;
&lt;h2&gt;
  
  
  5. How to Implement Audit Logging in Your Backend
&lt;/h2&gt;

&lt;p&gt;Suppose you're using Node.js, PostgreSQL, and a REST API.&lt;/p&gt;

&lt;p&gt;When an administrator assigns an asset to an employee, two actions must happen:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First:&lt;/strong&gt; Update the asset's current assignment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second:&lt;/strong&gt; Insert an audit record describing that assignment.&lt;/p&gt;

&lt;p&gt;These actions should ideally happen inside the same database transaction.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because you don't want a situation where the asset is updated successfully but the audit event fails to save.&lt;/p&gt;

&lt;p&gt;A simplified implementation might look like this:&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;assignAsset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;assetId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;employeeId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;actorId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BEGIN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SELECT assigned_to FROM assets WHERE id = $1 FOR UPDATE&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="nx"&gt;assetId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rowCount&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Asset not found&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;previousOwner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;assigned_to&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`UPDATE assets
       SET assigned_to = $1, updated_at = NOW()
       WHERE id = $2`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;employeeId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;assetId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`INSERT INTO asset_audit_logs
       (id, asset_id, action, actor_id,
        previous_value, new_value, created_at)
       VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, NOW())`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nx"&gt;assetId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ASSET_ASSIGNED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;actorId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;assigned_to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;previousOwner&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;assigned_to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;employeeId&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;COMMIT&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="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ROLLBACK&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This example assumes an initialized PostgreSQL connection pool, suitable table definitions, and an authenticated actor.&lt;/p&gt;

&lt;p&gt;The transaction ensures that the update and audit insertion either both succeed or both roll back.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;FOR UPDATE&lt;/code&gt; clause locks the selected asset row during the transaction, helping prevent concurrent assignment operations from reading and modifying the same old state.&lt;/p&gt;

&lt;p&gt;In a production application, you'd also validate permissions, confirm that the employee exists, enforce valid asset transitions, and handle retries or idempotency where necessary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The important design principle is simple: an important state change and its audit event should remain consistent.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Why Audit Trails Should Be Append-Only
&lt;/h2&gt;

&lt;p&gt;Here's another important design decision.&lt;/p&gt;

&lt;p&gt;Should administrators be allowed to modify previous audit events?&lt;/p&gt;

&lt;p&gt;Usually, ordinary application users should not.&lt;/p&gt;

&lt;p&gt;Imagine an administrator changes an asset assignment and later edits the historical event to hide the action.&lt;/p&gt;

&lt;p&gt;The audit trail would no longer be trustworthy.&lt;/p&gt;

&lt;p&gt;A better approach is an append-only event history.&lt;/p&gt;

&lt;p&gt;Instead of modifying an old record, create a new event that documents the correction.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Asset assigned to John&lt;/li&gt;
&lt;li&gt;Assignment identified as incorrect&lt;/li&gt;
&lt;li&gt;Assignment corrected to Sarah&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This preserves the sequence of events.&lt;/p&gt;

&lt;p&gt;However, append-only behavior at the application layer is not enough by itself.&lt;/p&gt;

&lt;p&gt;Production systems should also consider restricted database permissions, independent storage or replication for sensitive logs, defined retention policies, and monitoring for unauthorized changes.&lt;/p&gt;

&lt;p&gt;For stronger tamper evidence, organizations may use cryptographic integrity mechanisms or protected audit infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A trustworthy audit trail must be protected from the same actors whose actions it records.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Audit Trails Become Even More Important With Multiple Users
&lt;/h2&gt;

&lt;p&gt;Single-user applications are relatively easy to reason about.&lt;/p&gt;

&lt;p&gt;Multi-user applications introduce new challenges.&lt;/p&gt;

&lt;p&gt;Imagine two administrators opening the same asset record.&lt;/p&gt;

&lt;p&gt;Administrator A transfers the laptop to John.&lt;/p&gt;

&lt;p&gt;Administrator B still has the older screen open and assigns it to Sarah.&lt;/p&gt;

&lt;p&gt;Without concurrency controls, one update may overwrite the other.&lt;/p&gt;

&lt;p&gt;Even worse, the audit history may become confusing if the application records events without properly coordinating database changes.&lt;/p&gt;

&lt;p&gt;Depending on the system, developers can address this using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Database transactions&lt;/li&gt;
&lt;li&gt;Row-level locking&lt;/li&gt;
&lt;li&gt;Optimistic concurrency control&lt;/li&gt;
&lt;li&gt;Record version numbers&lt;/li&gt;
&lt;li&gt;Domain-specific state transitions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The objective is to ensure that asset history reflects the actual order of committed changes.&lt;/p&gt;

&lt;p&gt;Audit logging should be part of your application's data consistency strategy, not an afterthought.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. The Difference Between Event Sourcing and an Audit Trail
&lt;/h2&gt;

&lt;p&gt;These concepts are related, but they're not identical.&lt;/p&gt;

&lt;p&gt;With a traditional database and audit logs, your application stores the current asset state in one place and records relevant changes separately.&lt;/p&gt;

&lt;p&gt;With event sourcing, the event stream itself is the primary record of state changes, and current state is derived from those events.&lt;/p&gt;

&lt;p&gt;Event sourcing can offer powerful history and replay capabilities.&lt;/p&gt;

&lt;p&gt;But it also introduces additional complexity involving event versioning, projections, replay logic, and operational tooling.&lt;/p&gt;

&lt;p&gt;For many straightforward asset tracking applications, a conventional relational database with a well-designed audit log is entirely reasonable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You don't need event sourcing just to maintain useful change history.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Choose architecture based on actual requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Common Audit Logging Mistakes Developers Make
&lt;/h2&gt;

&lt;p&gt;Several implementation mistakes can undermine an otherwise useful audit system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recording only the latest value&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Without the previous relevant state, an event may not clearly explain what changed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trusting actor IDs supplied by the client&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The backend should derive the authenticated actor from the server-side security context, not blindly accept an arbitrary user ID from a request body.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Writing audit events outside the transaction&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This can create gaps when a state update succeeds but logging fails, or vice versa.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Logging sensitive information unnecessarily&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Audit events should not indiscriminately capture passwords, access tokens, personal information, or full request bodies.&lt;/p&gt;

&lt;p&gt;Use data minimization and applicable retention policies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Allowing normal users to delete audit records&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This weakens the reliability of historical evidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ignoring readability&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A thousand JSON records may be technically complete but difficult for IT administrators to understand.&lt;/p&gt;

&lt;p&gt;Consider presenting history in a human-readable activity timeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confusing audit history with live location tracking&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An event showing that an asset was scanned at a location does not prove that it remains there continuously.&lt;/p&gt;

&lt;p&gt;This distinction matters especially in physical equipment tracking.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Turning Audit Events Into Useful Product Features
&lt;/h2&gt;

&lt;p&gt;Once your application records meaningful asset events, you can build features that improve the everyday user experience.&lt;/p&gt;

&lt;p&gt;For example, an asset detail page might display:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MacBook Pro — LAP-1042&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Current custodian: John&lt;br&gt;&lt;br&gt;
Current status: Assigned&lt;br&gt;&lt;br&gt;
Recorded location: London Office&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Activity history&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Oct 11 — Assigned to John&lt;/li&gt;
&lt;li&gt;Oct 10 — Returned by Sarah&lt;/li&gt;
&lt;li&gt;Oct 2 — Assigned to Sarah&lt;/li&gt;
&lt;li&gt;Oct 1 — Added to inventory&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This timeline helps administrators understand the asset's recorded history without searching through separate spreadsheets or support tickets.&lt;/p&gt;

&lt;p&gt;You can also support structured audit reviews, assignment investigations, operational reporting, and equipment offboarding workflows.&lt;/p&gt;

&lt;p&gt;These are practical capabilities that emerge when a system records events as first-class business information.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. A Real-World Example: Employee Offboarding
&lt;/h2&gt;

&lt;p&gt;Let's connect the architecture to an everyday business process.&lt;/p&gt;

&lt;p&gt;An employee is leaving the company.&lt;/p&gt;

&lt;p&gt;The IT team needs to confirm which equipment is assigned to them.&lt;/p&gt;

&lt;p&gt;A system with only current-state records may provide the current assignment list.&lt;/p&gt;

&lt;p&gt;But a system with reliable asset history can also help answer:&lt;/p&gt;

&lt;p&gt;When was each device issued?&lt;/p&gt;

&lt;p&gt;Was the equipment transferred?&lt;/p&gt;

&lt;p&gt;Was a previous device returned?&lt;/p&gt;

&lt;p&gt;Who recorded the return?&lt;/p&gt;

&lt;p&gt;Were there any corrections or discrepancies?&lt;/p&gt;

&lt;p&gt;This historical context makes investigations easier and supports clearer equipment accountability.&lt;/p&gt;

&lt;p&gt;Physical asset management platforms increasingly emphasize these kinds of documented workflows.&lt;/p&gt;

&lt;p&gt;For example, Stackroom focuses on physical equipment records, employee custody, handovers, returns, and asset audits.&lt;/p&gt;

&lt;p&gt;For an example of the business workflows that auditability supports, see &lt;a href="https://www.stackroom.io/solutions/audits-compliance/" rel="noopener noreferrer"&gt;Stackroom's asset audits and compliance solution&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I am associated with Stackroom. The database design and code in this article are illustrative examples, not a description of Stackroom's internal implementation.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  12. A Simple Design Checklist Before Shipping
&lt;/h2&gt;

&lt;p&gt;Before releasing an audit-enabled asset tracking feature, check the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does every important asset change generate an event?&lt;/li&gt;
&lt;li&gt;Are the current-state update and audit insert atomic?&lt;/li&gt;
&lt;li&gt;Is the actor derived from an authenticated context?&lt;/li&gt;
&lt;li&gt;Do concurrent updates preserve a consistent history?&lt;/li&gt;
&lt;li&gt;Can ordinary users alter or delete historical events?&lt;/li&gt;
&lt;li&gt;Are sensitive values excluded where unnecessary?&lt;/li&gt;
&lt;li&gt;Can administrators understand the history without reading raw JSON?&lt;/li&gt;
&lt;li&gt;Are retention and access rules defined?&lt;/li&gt;
&lt;li&gt;Can the application explain who changed an asset, when, and what changed?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the answer to several of these questions is no, your application may be storing equipment data without maintaining a reliable account of how that data changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Building an asset tracking system isn't only about storing equipment information.&lt;/p&gt;

&lt;p&gt;It's about maintaining trust in that information.&lt;/p&gt;

&lt;p&gt;A database tells you the current status of an asset.&lt;/p&gt;

&lt;p&gt;An audit trail helps explain how that status changed.&lt;/p&gt;

&lt;p&gt;For applications involving employee assignments, equipment transfers, maintenance, and offboarding, that distinction becomes extremely important.&lt;/p&gt;

&lt;p&gt;You don't need an unnecessarily complicated architecture to get started.&lt;/p&gt;

&lt;p&gt;A well-designed audit table, consistent transactions, authenticated actions, and sensible access controls can provide a strong foundation.&lt;/p&gt;

&lt;p&gt;As the application grows, that foundation makes troubleshooting and operational accountability much easier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A good asset tracking system tells you where things stand. A great one helps you understand how they got there.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Over to you
&lt;/h3&gt;

&lt;p&gt;When building business applications, do you prefer maintaining a separate audit table, using database triggers, or adopting event sourcing?&lt;/p&gt;

&lt;p&gt;What trade-offs have you encountered in production?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>backend</category>
      <category>database</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
