A zero-install static site hosting service for hackers

Table of Contents

The easiest way to deploy static sites on the web.

NOTICE: This is a premium pico+ service with a tiny free tier

Features #

Publish your site with one command #

When your site is ready to be published, copy the files to our server with a familiar command:

1rsync -rv public/

That's it! There's no need to formally create a project, we create them on-the-fly. Further, we provide TLS for every project automatically.

Manage your projects with a remote CLI #

Use our CLI to manage projects:

1ssh help

Instant promotion and rollback #

Additionally you can setup a pipeline for promotion and rollbacks, which will instantly update your project.

1ssh link project-prod project-d0131d4

A common way to perform promotions within is to setup CI/CD so every git push to main would trigger a build and create a new project based on the git commit hash (e.g. project-d0131d4).

This command will create a symbolic link from project-prod to project-d0131d4. Want to rollback a release? Just change the link for project-prod to a previous project.

We also built a github action that handles all the logic for uploading to Here's an example of it in action.

CLI Reference #

The best way to learn about all the commands we support is via an SSH command:

1ssh help

Having said that, we do want to demonstrate the power of by discussing design goals. All of our SSH commands are safe-by-default. Meaning, they never mutate server state by default. This provides users an opportunity to experiment with our commands to see how they work. In order to actually trigger server mutations, every command must be appended with --write.

Further, we want to make sure users are able to manage their static sites exclusively from SSH commands. Below is list of features we support via SSH commands:

 1# storage usage stats
 2ssh stats
 4# list all projects (and their links)
 5ssh ls
 7# list all project dependencies
 8ssh depends project-x
10# link a project (e.g. folder symlink)
11ssh link project-x --to project-y
13# unlink a project
14ssh unlink project-x
16# delete a project
17ssh rm project-x
19# delete all projects matching a prefix
20# (except projects that have linked projects)
21ssh prune prefix
23# delete all projects matching a prefix
24# except the last (3) recently updated projects
25ssh retain prefix
27# set project to private to only you and matching public keys
28ssh acl project-x --type pubkeys --acl sha256:xxx

File denylist #

You can upload any file you want to pages, with a few exceptions.

Because any file uploaded to pages is public-by-default, we felt it necessary to automatically reject some files from being uploaded. At this point in time, we reject all files or files inside directories that start with a period .. Essentially, we reject all dotfiles. This is so users don't accidentally upload a .git folder or .env files. This is the equivalent rule in our .gitignore parser:


Override denylist #

Upload a _pgs_ignore to the root of each project. We are using the same rules as .gitignore using this parser.

If you want to allow all files without ignoring anything, add a _pgs_ignore with any comment (to get around how we handle 0-byte files):

# dont ignore files

Note: when uploading a _pgs_ignore, we cannot guarentee it will be uploaded first so we recommend uploading it on its own and then upload the rest of your site.

Pretty URLs #

By default we support pretty URLs. So there are some rules surrounding URLs that are important to understand.

For the route https://{user}-{project}, we will check for the following files:

  • /space
  • /space.html
  • /space/: 301 redirect to /space/index.html
  • /404.html

As you can see from the third entry, we add a trailing slash to all routes. This is a common practice with static sites in order to prevent having different behavior from visiting a site with and without a trailing slash.

Custom Domains #

We have a very easy-to-setup guide on custom domains.

Custom Redirects and rewrites #

We support custom redirects and rewrites via a special file _redirects.

# Redirect browser request to what we serve
/home                /
/blog/post.php       /blog/post
/news                /blog
/authors/c%C3%A9line /authors/about-c%C3%A9line

When no status is provided, we default to 301 Moved Permenantly.

# Redirect with a 301
/home         /              301

# Redirect with a 302
/my-redirect  /              302

# Show a custom 404 for this path
/ecommerce    /store-closed  404

# Rewrite a path
/pass-through /index.html    200

Route Shadowing #

By default we do not shadow routes that exist. For example:

/space.html exists on your site

With a _redirects entry:

/space   /   301

If the user goes to /space then it will always prefer /space.html. You can override this preference by adding a force flag to your redirect entry:

/space   /   301!

Redirect www to naked domain #

Our recommended solution is to create a separate project with just a _redirects file inside of it.

  1. Create a _redirects file with a 301 to naked domain:
1echo "/*  301" >> _www_redirects
2rsync _www_redirects
  1. Add a www CNAME and TXT record to point to www project

See our custom domains page.

Rewrites #

When you assign an HTTP status code of 200 to a redirect rule, it becomes a rewrite. This means that the URL in the visitor’s address bar remains the same, while pico's servers fetch the new location behind the scenes.

With _redirects we also support rewrite rules for when you want to show content from another site without a full URL redirect.

This can be useful for single page apps, proxying to other services, proxying to other pgs sites, or transitioning for legacy content.

Here are some examples:

/* 200
/my-site/* 200
/news/:month/:date/:year/*  /blog/:year/:month/:date/:splat 200

Proxy to another service #

Similar to how you can rewrite paths like /* to /index.html, you can also set up rules to let parts of your site proxy to external services. Let’s say you need to communicate from a single-page app with an API on that doesn’t support CORS requests. The following rule will let you use /api/ from your JavaScript client:

/api/*  200

Limitations #

  • Infinitely looping rules, where the "from" and "to" resolve to the same location, are incorrect and will be ignored.
  • By default, we limit internal rewrites to one "hop".
  • Rewrites can cause pages that use assets specified through relative paths to load incorrectly. To make sure your site's proxied content is displayed as expected, use absolute paths for your assets.
  • Paths handled by proxies may not redirect from HTTP to HTTPS URLs as expected. If you’re working with proxies, we recommend only publishing HTTPS URLs for your visitors to use.

Custom Headers #

We support custom headers via a special file _headers.

# a path:
  # headers for that path:
  X-Frame-Options: DENY
  X-XSS-Protection: 1; mode=block
# another path:
  # headers for that path:
  X-Frame-Options: SAMEORIGIN
  X-Frame-Options: DENY
  X-XSS-Protection: 1; mode=block

Denied Headers #

These headers are not allowed:


Single-Page Applications #

We support SPAs! Upload a _redirects file to your project:

/*  /index.html  200

Reserved username project #

If you create a project with the same name as your username, then you can access it at:

1rsync -rv public/
2# =>

Content security policy #

For pico domains we have modestly strict content-security policies.

1Content-Security-Policy "default-src 'self'; img-src * 'unsafe-inline'; style-src * 'unsafe-inline'"

If you need to access sites that are blocked by this CSP, then you can use a custom domain which won't have those security restrictions.

Access Control List #

Thanks to SSH tunnels we can provide restricted access to projects.

We have three options:

  • public (default)
  • pubkeys (list of sha256 public keys to give read access to)
  • pico (list of pico users to grant read access to)
 1# access to anyone with a public key
 2ssh acl project-x --type pubkeys 
 4# access only to public keys provided
 5ssh acl project-x --type pubkeys --acl sha256:xxx --acl sha256:yyy
 7# access to anyone with a pico account
 8ssh acl project-x --type pico
10# access only to pico users provided
11ssh acl project-x --type pico --acl antonio --acl erock
13# access to anyone
14ssh acl project-x --type public

To connect to a private project:

1ssh -L 1337:localhost:80 -N {subdomain}
3# for example our pico UI is only available through an SSH tunnel:
4ssh -L 1337:localhost:80 -N

Then open your browser to http://localhost:1337

Does pages have a CDN or multi-region support? #

At this point in time, we are able to serve content from a single VM. If this service gains traction we will commit to having a CDN with multiple regions in the US.

Ready to join pico?

Create an account using only your SSH key.

Get Started