Dimensión Segura

An e-commerce platform for hazardous materials signage that evolved into a platform featuring a hazmat database built from official sources, accessible via a private API and searchable from the public website.

From an e-commerce site to a hazmat data platform

Dimensión Segura (which roughly translates to Safe “Cargo” Dimensions in the context of dangerous goods transportation) started as a website to sell hazardous materials labels that complies with Mexican regulations. The first version was an e-commerce site and a blog built on WordPress and WooCommerce, which was sufficient to test the market and begin operations.

By observing buyer behavior and the received feedback, I identified a clear pattern: official information on hazardous materials is fragmented and difficult to match with the correct product. From there, I built a second system: a relational database powered by scrapers and exposed via a private API, that complements the e-commerce site and gives the platform value beyond just the product catalog.

Use case: Search “ammonia” in Dimension Segura

Hazmat information is scattered and difficult to match with the correct product

People who purchase hazardous material labels are doing it because they must comply with a regulation. The real challenge becomes knowing which sign corresponds to a specific substance or UN number, along with its hazard class and division, and which pictogram to use.

That information exists, but it’s scattered across:

  • Official lists of substances and UN numbers.
  • Classification tables by hazard class and division.
  • Pictogram and labeling specifications.

Each source has its own format, structure, and level of detail. For the end user, finding the right product meant opening multiple tabs and cross-referencing them manually. The opportunity lay in centralizing that cross-referencing and making it just a click away from the site’s search function.

Two systems coexisting within the same project

The final architecture consists of two layers that share infrastructure but serve different purposes:

  • Commercial layer: WordPress + WooCommerce for the product catalog, shopping cart, checkout, blog, and email marketing.
  • Data layer: MySQL database (previously populated by Python scraper), and a private API (Flask with an additional PHP endpoint within WordPress) that exposes queries to the public frontend.

The decision to stick with WordPress and not migrate everything to a custom stack was a deliberate one: WooCommerce already handled payments, inventory, coupons, and transactional emails. Rewriting that code to make it “cleaner” would have been a pain in the ah- task. Instead, the logic where I could actually add value, the hazmat data, was built separately and integrated as a service that the site consumes.

[VPS running the WordPress site]
 |
 |-/un/{UN_CODE} -> Server-Side Rendering with PHP
 |
 |
[Cloudflare]
 |
[Private Server running the API]
 |
[nginx] -> /search, /detail -> Flask (Python) -> MySQL

A relational model designed for joins

I decided to use relational MySQL because hazmat data fits much better into a normalized schema than into a document. A substance has a UN number, a class, one or more divisions, one or more names, and an associated pictogram. Modeling it as related tables makes queries straightforward and ensures that data integrity can be verified.

In simple terms, the plan revolves around:

  • un_number (self explanatory, ~2500 entries)
  • name (official names and synonyms, ~3600 entries)
  • risk (all 50 risks and classes, with code, description, slugs, among others)
  • erg (64 emergency response guidelines)
  • special_provision (~200 special provisions in the Mexican regulations)
  • un_entry (packing group, limited quantities and excepted quantities)

That being said, all tables relate to the tables un_number or un_entry with additional tables:

  • un_name (un_number y name)
  • un_risk (un_number y risk)
  • un_erg (un_number y erg)
  • un_entry_special_provision (un_entry y special_provision)

Scrapping and Standardization

The scrapers to populate the database were written in Python using requests and BeautifulSoup. Each source has its own script because each has its own unique quirks: tables with merged headers, UN numbers with inconsistent prefixes, descriptions with line breaks within cells, and abbreviations that vary from page to page.

The workflow is always the same:

  1. Download: Raw HTML from federal resources to my local machine. If the source changes, keeping the HTML allows me to reprocess it without hitting the server again.
  2. Parsing: extraction into Python structures (list / dict) with expected fields.
  3. Normalization: cleaning strings (non-breaking spaces, typographic quotes), standardizing UN numbers to the UN0000 format, mapping classes and divisions to the IDs in my table.
  4. Validation: checks before touching the database (UN number exists in international references, class exists, no duplicates).
  5. Upsert: insert or update in MySQL, source and scrape date recorded.

Standardization was the part that took me the longest and involved the most errors. The same substance is written in three different ways across sources; the synonyms were compiled into their own table so that the site’s search function can resolve them without requiring any special logic on the front end.

Flask for data, custom PHP for integration with WordPress

The private API is built using Flask. The endpoints are hosted behind /api/ in Nginx and accept requests to retrieve. Here there is a simplified version of the response (the values are in Spanish because the e-commerce itself is in Spanish as well):

GET /api/detail?code=1274
-> {
 'un_code': '1274',
 'official_names': 'n-PROPANOL',
 'optative_names': 'ALCOHOL PROPÍLICO NORMAL',
 'synonyms': None,
 'pr_code': '3',
 'pr_description': 'Líquidos inflamables',
 'erg_description': '(mezclables con agua / nocivo)',
 'erg_edition': 2024,
 'erg_guide': 129,
 'pg_codes': [{'eq_code': 'E2',
               'eq_inner': 30,
               'eq_outer': 500,
               'lq': '1 L',
               'pg_code': 'II',
               'special_provision': []},
              {'eq_code': 'E1',
               'eq_inner': 30,
               'eq_outer': 1000,
               'lq': '5 L',
               'pg_code': 'III',
               'special_provision': ['223']}]
}

The API is not publicly available in the broad sense. It only allows calls from the site itself using a private key. I did not publish open documentation because the use case is to serve the e-commerce frontend, not to function as a third-party dataset.

On the WordPress side, I wrote a short hook that registers the response from the API and populates a PHP template. This allows Server-Side Rendering, facilitating proper indexing by search engines to improve SEO.

All data from the request is seamlessly rendered into the UI

Interesting Technical Challenges

  • Sources that change without warning. More than once, a scraper broke because the source changed a table header. I solved this with two approaches: schema checks before parsing (if the expected columns aren’t there, the script fails cleanly instead of inserting garbage into the database), and saving the raw HTML so I could use diff to see what had changed.
  • Synonym normalization. A substance appears as “Cinc,” “Zinc,” and “Zn” in different sources. Modeling all the names logic (including synonyms) as a separate table made the search consistent and fast.
  • Integrating Flask and WordPress without coupling them. The initial temptation was to have WordPress query the hazmat tables directly in MySQL. I ruled that out: it broke isolation and forced me to duplicate business logic in PHP. Passing everything through the Flask API costs one HTTP call, but leaves a single place where the logic for how the data is cross-referenced resides.
  • Considering security at all levels. For the system to function properly without vulnerabilities, I followed security rules from the beginning: use parameterized database queries, skip ambiguous queries, store the API key in environment variables, use esc_html(), esc_url(), esc_attr(), and wp_kses() within the PHP template that renders the result, among others.

Results and Achievements

  • The site is live and operating at https://dimensionsegura.com, the database at https://dimensionsegura.com/consulta-un.
  • Actual sales were made just a few months after the initial e-commerce launch.
  • Email marketing automation is active and triggers sequences based on real store events.
  • The private API handles traffic from the public frontend without having required any rewriting since its first stable release.
  • I am not including traffic or revenue metrics because they are not the focus of this case. What matters is that the architecture supports a functioning business and a data system that requires minimal ongoing maintenance.

What did I learn?

I worked on this project entirely on my own, from start to finish: identifying the problem, deciding on the architecture, writing the code, designing the UI, setting up the infrastructure, operating it, and fixing it when it breaks. That forces me to make different decisions than I would within a team with separate roles.

Three things became clear to me:

  1. The right architecture is the simplest one that solves the problem. Sticking with WooCommerce instead of reinventing checkout, using a single MySQL database instead of two, not adding layers of orchestration. Every extra piece incurs an operational cost every day.
  2. Data is just as important as code. The real work of the project wasn’t writing Flask; it was normalizing heterogeneous data sources until they conformed to a consistent schema. Without that, the API would have nothing useful to return.
  3. Building, launching, and operating teaches things that reading does not. I learned WordPress, PHP, scraping, relational modeling, nginx, and deployment because each step demanded it. On the fly. Self-taught learning driven by real-world problems.