Skip to content
GEOSAT
Back to blog
Open GIS
Open GIS2026-09-23GEOSAT6 min read

Publish a PostGIS layer with GeoServer and verify WMS and WFS

Publish a PostGIS fixture through GeoServer WMS/WFS, verify identifiers and rendered output, and diagnose CRS, store and client failures.

Editorial review: 2026-09-23

GeoServerPostGISOpen GIS

Outcome: publish a synthetic PostGIS layer through WMS and WFS, inspect the returned geometry and image, and connect a desktop consumer. This is the first service-level migration test: a map that looks correct does not prove that identifiers, attributes, permissions or queries survived.

Use a disposable database and a local GeoServer instance. The downloadable Open GIS lab provides synthetic inputs and records what has actually been executed. The SQL below is an independent, minimal fixture; it creates a schema called publish_lab. Do not run it against a customer database or confuse its names with another lab's schema.

1. Define the public contract before publishing

Write the contract in plain language: “An anonymous reader can see two synthetic parcels, retrieve their identifiers and status, and draw them in the expected location. The reader cannot edit records or retrieve owner names.” This immediately separates a public presentation layer from a master cadastral table.

Contract elementThis exerciseProduction decision
Dataset identitypublish:parcelsStable name and deprecation policy
IdentifierInteger parcel_idStable across reloads and exports
GeometryPolygon, EPSG:4326Actual source CRS and required transformations
Public attributesparcel_id, statusExplicit list approved by data owner
AccessRead onlyAnonymous, group or partner scope
ConsumersQGIS plus HTTP requestsExact application and version inventory

GeoServer publishes services over data sources; it does not automatically recreate ArcGIS web maps, item sharing, forms or application configuration. Record those dependencies separately in the service migration matrix.

2. Create a small, predictable source

The database must already have the PostGIS extension installed by its administrator. Run this fixture as a role allowed to create objects in the disposable database. The coordinates are invented near Medellín and do not identify real property boundaries.

Code example
CREATE SCHEMA publish_lab;
CREATE TABLE publish_lab.parcels (
  parcel_id integer PRIMARY KEY,
  status text NOT NULL CHECK (status IN ('review', 'approved')),
  geom geometry(Polygon, 4326) NOT NULL
);
INSERT INTO publish_lab.parcels VALUES
(1, 'review', ST_GeomFromText(
 'POLYGON((-75.572 6.238,-75.570 6.238,-75.570 6.240,-75.572 6.240,-75.572 6.238))',4326)),
(2, 'approved', ST_GeomFromText(
 'POLYGON((-75.569 6.241,-75.567 6.241,-75.567 6.243,-75.569 6.243,-75.569 6.241))',4326));
CREATE INDEX parcels_geom_idx ON publish_lab.parcels USING gist(geom);
ANALYZE publish_lab.parcels;
SELECT count(*) AS rows, count(DISTINCT parcel_id) AS unique_ids,
       bool_and(ST_IsValid(geom)) AS valid,
       min(ST_SRID(geom)) AS srid
FROM publish_lab.parcels;

Expected values are 2, 2, true and 4326. A successful INSERT is insufficient: check the count, key and geometry before debugging the map server. The PostGIS validation guide extends this to imported files.

Have the database administrator provision a separate login for GeoServer. Grant only CONNECT to the lab database, USAGE on publish_lab, and SELECT on this table. Avoid the table owner or a superuser. Keep the password in your deployment secret store or local connection configuration; never place it in a public tutorial, connection URL or Git repository. A later write endpoint needs a separate, deliberately tested role.

3. Configure workspace, store and layer

In GeoServer's administration interface, create workspace publish with namespace URI urn:geosat:publish-lab. The URI is an identifier, not a promise that a website exists there. Add a PostGIS store inside the workspace. Enter host, port, database, schema publish_lab and the read-only account. Test the connection before publishing.

Choose parcels from the new store and publish it. Confirm the native name, geometry column and detected primary key. Set the declared CRS to EPSG:4326 only because the fixture really uses longitude and latitude in that CRS. Compute the native and geographic bounding boxes from the data. If a real dataset appears far from its country, investigate its source reference system; relabeling coordinates is not a transformation.

Assign a polygon style and save. The first style should be deliberately simple. Complex labels, filters and scale rules create additional failure modes; introduce them after the data contract works. GeoServer's PostGIS publishing tutorial explains the store and publication screens.

4. Check discovery and the feature contract

These requests assume local GeoServer at port 8080. Inspect the workspace-specific capabilities document before copying a URL to a client:

Code example
curl --fail --get 'http://127.0.0.1:8080/geoserver/publish/ows' \
  --data-urlencode 'service=WFS' \
  --data-urlencode 'version=2.0.0' \
  --data-urlencode 'request=GetCapabilities' --output capabilities.xml

curl --fail --get 'http://127.0.0.1:8080/geoserver/publish/ows' \
  --data-urlencode 'service=WFS' \
  --data-urlencode 'version=2.0.0' \
  --data-urlencode 'request=GetFeature' \
  --data-urlencode 'typeNames=publish:parcels' \
  --data-urlencode 'count=1' \
  --data-urlencode 'outputFormat=application/json' --output parcel.json

Open the JSON. Expect a FeatureCollection with one polygon, a stable feature identifier, and the intended public fields. Repeat with a count large enough to retrieve both records and compare identifiers to the SQL result. In a real migration, compare nulls, date/time representation, decimal precision and enumeration values as well. Test an unknown layer name and a missing identifier: errors must not return a different layer or expose internal credentials.

WFS 2.0 uses typeNames and count; do not transfer parameter names blindly from another WFS version. The WFS reference documents the request contract. An HTTP 200 response can still contain an OGC exception, so inspect content type and body.

5. Check the map image independently

Code example
curl --fail --get 'http://127.0.0.1:8080/geoserver/publish/wms' \
  --data-urlencode 'service=WMS' \
  --data-urlencode 'version=1.1.1' \
  --data-urlencode 'request=GetMap' \
  --data-urlencode 'layers=publish:parcels' \
  --data-urlencode 'styles=' \
  --data-urlencode 'srs=EPSG:4326' \
  --data-urlencode 'bbox=-75.574,6.236,-75.565,6.245' \
  --data-urlencode 'width=600' \
  --data-urlencode 'height=600' \
  --data-urlencode 'format=image/png' --output parcels.png

Open parcels.png; it should show two polygons inside the requested extent. This request deliberately uses WMS 1.1.1 with longitude/latitude bounding-box order. If you change to 1.3.0, review CRS and axis ordering rather than changing only the version string. The WMS protocol reference is the source for those differences.

Add a WMS connection in QGIS using the service root, then select publish:parcels. Add the WFS separately. Pan, inspect the feature table, identify a parcel and export a small result. If ArcGIS Pro remains a consumer, repeat there: root discovery, layer selection, extent, drawing and identify. Server reachability is not evidence of successful client rendering.

6. Diagnose from the failing layer outward

SymptomFirst checkNext action
Store cannot connectHost, network, database roleTest a read query using the same role
WFS works, WMS blankExtent, CRS, styleUse the exact fixture bbox and plain polygon style
Image file contains XMLOGC exception bodyRead the exception before changing server settings
Features appear in the wrong countryActual coordinate valuesDetermine source CRS; transform coordinates correctly
Features change IDs after reloadSource key and import methodPreserve a stable business identifier
QGIS works, another client failsRequested version and parametersCapture and replay that client's request
Private attributes appearPublished schema and service permissionsPublish an approved view and retest every access path

7. Decide what counts as accepted

Keep the SQL checks, requests, returned sample and a screenshot from each supported client with the version used. Acceptance requires both positive and negative tests: authorized reads work, unauthorized reads and edits fail, unknown resources return useful errors, and limits prevent unbounded exports. Add security and proxy configuration, then a backup and restore exercise before public rollout.

This guide is a publication procedure, not a performance benchmark or a claim that every GeoServer/ArcGIS combination has been tested. For a migration estimate, bring the service inventory, consumer versions, data volume, refresh frequency and access model to the GeoSAT assessment. Those inputs determine the work; the number of layers alone does not.

Related articles