GAP School Module 02 — Inventory OS Lesson 2.1

The custom post type is the data model everything else is built on. Get the schema right and every downstream feature — feeds, AI descriptions, search, filtering — has clean data to work with. Get it wrong and you spend the next 18 months apologizing for it in every adjacent feature you build.


The situation

Most WordPress shops register custom post types in functions.php. It works — until the client wants to change themes, or until you swap to a child theme, or until the theme breaks and you have to nuke it. When the CPT registration lives in functions.php, it's tied to the theme. The data model lives and dies with a presentation layer decision.

For the Anchor build, the inventory CPT had 38 fields. Year, make, model, condition, price, stock number, VIN, length, beam, engine hours, fuel type, hull material, and 25 more. These fields are business data. They belong in the business layer — the plugin — not in the theme.

The other common mistake is reaching for ACF (Advanced Custom Fields) as the schema layer. ACF is powerful, but it becomes a hard dependency. The field schema is stored in the ACF internal format, behind ACF's admin UI. Migrating away from ACF later means exporting field groups, mapping meta keys, and updating every template that calls get_field(). That migration never happens — so you're locked to ACF and its licensing model indefinitely.


What I did

Register the CPT in the plugin, not functions.php

The plugin is where business logic lives. Theme changes are presentation changes. Never let a presentation change destroy a data model.

CPT registration — inside plugin main file
add_action( 'init', '[client]_register_inventory_cpt' ); function [client]_register_inventory_cpt() { register_post_type( '[client]_inventory', [ 'labels' => [ 'name' => 'Inventory', 'singular_name' => 'Unit', 'add_new_item' => 'Add New Unit', 'edit_item' => 'Edit Unit', ], 'public' => true, 'has_archive' => true, 'rewrite' => [ 'slug' => 'inventory', 'with_front' => false, ], 'supports' => [ 'title', 'editor', 'thumbnail', 'excerpt' ], 'menu_icon' => 'dashicons-store', 'show_in_rest' => true, ] ); }

The rewrite slug decision matters. Set it once, never change it — every existing URL for every listing is based on this slug. with_front => false prevents WordPress from prepending /blog/ or whatever the site's front page slug is to every listing URL.

Field schema with client-prefixed meta keys

Client-prefix every meta key. This prevents collisions with WooCommerce, Yoast, theme frameworks, and every other plugin that writes to wp_postmeta. [client]_price is unambiguous. price is a collision waiting to happen.

Field schema — register_post_meta() in plugin
add_action( 'init', '[client]_register_inventory_meta' ); function [client]_register_inventory_meta() { $fields = [ '[client]_price' => 'number', '[client]_stock_number' => 'string', '[client]_year' => 'integer', '[client]_make' => 'string', '[client]_model' => 'string', '[client]_condition' => 'string', '[client]_vin' => 'string', '[client]_length_ft' => 'number', '[client]_engine_hours' => 'integer', '[client]_fuel_type' => 'string', '[client]_hull_material' => 'string', '[client]_sort_priority' => 'integer', // ... remaining fields ]; foreach ( $fields as $key => $type ) { register_post_meta( '[client]_inventory', $key, [ 'type' => $type, 'single' => true, 'show_in_rest' => true, ] ); } }

Why register_post_meta() instead of ACF

Native register_post_meta() stores data as standard WordPress post meta — readable by get_post_meta(), writable by update_post_meta(), queryable via WP_Query meta_query, and exposed in the REST API with show_in_rest => true. No plugin dependency. No licensing. No schema export format to worry about.

The schema is defined in one PHP array. When you need to add a field, you add a line to the array and optionally run a migration to backfill existing posts. That's it.


Why it matters

The CPT schema is the contract between the data layer and everything built on top of it. The Google Sheets sync (Lesson 2.2) writes to these meta keys. The sort priority logic (Lesson 2.4) queries by these meta keys. The AI description feature reads from these meta keys. The marketplace feed exports these meta keys. Every downstream system depends on the schema being stable and predictable.

Registering in the plugin means the schema survives theme changes, child theme swaps, and theme debugging. It's always on because the plugin is always on.

Native register_post_meta() with typed fields means WordPress validates values at write time. A number field won't accidentally store a string. An integer field won't store a float. The schema enforces its own integrity.


The Anchor build

The inventory CPT has been in production since Era 1 without a schema redesign. Fields have been added — the initial schema had 24 fields; it grew to 38 as the business's reporting needs became clear. Adding fields was a one-line change each time: add to the $fields array, run a backfill script on existing posts if needed, done.

The show_in_rest => true registration proved critical when the Google Sheets sync was built. The Apps Script sync uses the REST API to read and write post meta. Without REST registration, the sync would have required a custom endpoint. With it, the standard /wp/v2/[client]_inventory/{id} endpoint handles reads and writes natively.


Do this, not that

  • Register your CPT in the plugin, never in functions.php. The plugin is the business layer. The theme is the presentation layer. Data belongs in the business layer.
  • Client-prefix every meta key. [client]_price will never collide with WooCommerce's _price. Unambiguous keys prevent hours of debugging later.
  • Use native register_post_meta() instead of ACF. ACF is a hard dependency with its own data format. Native meta is standard WordPress, readable by any code, queryable by WP_Query, and carries no licensing risk.
  • Set show_in_rest => true from day one. REST API access is required for the Sheets sync, for any external integrations, and for Gutenberg block access. Adding it later is the same one-line change — but if you forget, external callers break silently.
  • Set the rewrite slug once and leave it. Every listing URL is based on this slug. Changing it after launch means 301-redirecting hundreds or thousands of existing URLs.
When you’re ready to build

The lessons are yours. When you want it built, we’re here.

Every lesson stays free — no account, no paywall, no email gate, ever. But if you’d rather have this system standing on your business than wire all 48 lessons yourself, leave your email. We’ll send you a direct line to a build — and you’ll be first to hear when we add new tools to the curriculum.

None of this gates a single lesson. The curriculum was free before you got here and it stays that way.

We’ll use your email to send you a fast-track to a GAP build and occasional notes on how GAP builds digital sales departments. Lessons stay 100% free — no email required to read any of them. We never share or sell your information. Unsubscribe any time. Privacy policy at gapindustriesllc.com/privacy.html.

Done learning how it’s built? We’ll build it.

You came here to understand the system, and now you do. If you’d rather have it standing on your business than spend the next three months wiring it yourself, GAP Concierge is the same architecture from these lessons — a white-label AI agent that knows your catalog and captures your leads — set up for you, from $97/mo.

See GAP Concierge →