Skip to main content

ยท One min read
Feodor Fitsner

We are thrilled to announce Pglet v0.6.0 and updated Python client!

Notable changes:

  • Added focused property, on_focus and on_blur events to all input controls - paving the way to a proper validation support.
  • New Persona control.
  • New ComboBox control.
  • New page events: connect and disconnect for real-time chat-like experiences.
  • Harmonization of border styling propeties across Stack, Image, IFrame and Text controls: HTML-ish border property with mixed and confusing to non-web devs semantics (1px solid black or solid 1px black?) replaced with clean and simple border_style, border_width and border_color properties.
  • All boolean and enum-like properties are protected with beartype.
  • Fixed all Python tests to ensure Pglet works nice with Python 3.7 and above. Big shout-out to @mikaelho for helping with that!
  • Black and isort was adopted as official Python formatting tools.
  • Generating platform-specific wheels (.whl) with one pglet executable inside only: smaller wheels - faster installation!

ยท 11 min read
Feodor Fitsner


Staying in touch with your users via email is still an effective and reliable communication channel. In this tutorial we are going to implement email signup form for a React-based static website that allows users to submit their email address and subscribe to a project mailing list. We are going to implement "double opt-in" process where upon signup an email is sent to the user which includes a link to click and confirm the subscription.

Pglet website is made with Docusaurus and hosted on Cloudflare Pages. However, the following solution could be easily adopted for other React-based website frameworks such as Next.js and use a different backend for server-side logic such as Vercel Functions or Deno Deploy.

Project requirements:

  • The form must be as simple as possible: just "email" field and "submit" button.
  • The form must protected by CAPTCHA.
  • Double opt-in subscription process should be implemented: after submitting the form a user receives an email with a confirmation link to complete the process.

For CAPTCHA we are going to use hCaptcha, which is a great alternative to Google's reCAPTCHA and has a similar API.

A signup form requires server-side processing and for that we re going to use Cloudflare Pages Functions which are a part of Cloudflare Pages platform.

For maintaining mailing list and sending email messages we are going to use Mailgun. Mailgun offers great functionality, first-class API at a flexible pricing, plus we have a lot of experience with it.

All code samples in this article can be found in:

Email signup formโ€‹

Signup form is implemented as a React component and includes an email entry form with hCaptcha and two messages:

The official hCaptcha demo React app with invisible captcha was a perfect starting point for making our own Docusaurus component.

Add hCaptcha component to your project:

yarn add @hcaptcha/react-hcaptcha --save

Create src/components/signup-form.js with the following contents:

import React, { useEffect, useRef, useState } from "react";
import BrowserOnly from '@docusaurus/BrowserOnly';
import HCaptcha from "@hcaptcha/react-hcaptcha";

export default function SignupForm() {
const [token, setToken] = useState(null);
const [email, setEmail] = useState("");
const captchaRef = useRef(null);

const onSubmit = (event) => {

useEffect(async () => {
if (token) {
var data = {
email: email,
captchaToken: token

// send message
const response = await fetch("/api/email-signup", {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
}, [token, email]);

return (
<div id="signup" className="signup-form">
<BrowserOnly fallback={<div>Loading...</div>}>
{() => {
if (token) {
// signup submitted
return <div>Thank you! You will receive the confirmation email shortly.</div>
} else if (window.location.href.endsWith('?signup-confirmed')) {
// signup confirmed
return <div><span style={{fontSize:'25px', marginRight:'10px'}}>๐ŸŽ‰</span>Congratulations! You have successfully subscribed to Pglet newsletter.</div>
} else {
// signup form
return <form onSubmit={onSubmit}>
<h3>Subscribe to Pglet newsletter for project updates and tutorials!</h3>
placeholder="Your email address"
onChange={(evt) => setEmail(}
<input type="submit" value="Submit" />

It's simply <form> element with "email" and "submit" inputs - except hCaptcha, no other 3rd-party components or hooks were used.

Replace {YOUR-HCAPTCHA-SITE-KEY} with your own hCaptcha site key.

Captcha is verified on form.onSubmit event which supports submitting form with ENTER and triggers built-in form validators. The result of captcha verification is stored in token state variable which is sent to /api/email-signup server function along with entered email for further verification and processing.

Add signup-form.js component to src/pages/index.js page:

import SignupForm from '@site/src/components/signup-form'

and then put <SignupForm/> inside <main> element:


When you run Docusaurus site with yarn start and navigate to a page with captcha at http://localhost:3000 you'll get "blocked by CORS policy" JavaScript errors. To make captcha work locally you should browse with a domain instead of "localhost".

Add a new mapping mysite.local to sudo nano /private/etc/hosts and then you can open http://mysite.local:3000 with working captcha.


A part of form component is wrapped with <BrowserOnly> element which tells Docusaurus that the contents inside <BrowserOnly> is not suitable for server-side rendering because of client-side API used, in our case window.location.ref. You can read more about <BrowserOnly> here.

Configuring Mailgunโ€‹

Mailgun is a transactional email service that offers first-class APIs for sending, receiving and tracking email messages.


We are not affiliated with Mailgun - we just like their service and have a lot of experience with it.

Some advice before creating a mailing list in Mailgun:

  • Start with a free "Flex" plan - it allows sending 5,000 messages per month and includes custom domains.
  • Configure custom domain - of course, you can test everything on a built-in {something} domain, but messages sent from it will be trapped in recipient's Junk folder. Custom domain is included with a free plan and setting it up is just a matter of adding a few records to your DNS zone.
  • Get dedicated IP address - if you require even greater email deliverability, assign your domain to a dedicated IP address. Dedicated IP is part of "Foundation" plan which starts at $35/month.

Cloudflare Pages Functionsโ€‹

Cloudflare Page Functions are based on Cloudflare Workers.

Be aware that Functions runtime environment is different from Node.js - you can't use Node.js built-in modules, you can't install anything from NPM. It's more like JavaScript in a headless browser with fetch(), WebSocket, Crypto and other Web APIs.

For signup form, we are going to add two functions:

  • POST /api/email-signup - for initial form processing and signup
  • GET /api/confirm-subscription?email={email}&code={code} - for confirming subscription

To generate routes above, we need to create two files: /functions/api/email-signup.js and /functions/api/confirm-subscription.js in the project repository.


/functions directory must be in the root of your repository, not in /static directory, and must be published along with the site.

You can glance through Functions docs to become familiar with the technology. Here I'll only cover some tricky issues which could arise while you develop.

First, it's possible to run and debug your functions locally. A beta version of Wrangler tool should be installed for that:

yarn add wrangler@beta --save-dev

Disregard scary deprecation warning while looking for wrangler package on and don't install @cloudflare/wrangler as it suggests. Apparently, Cloudflare team is actively working on Wrangler v2 and publishes it as wrangler package.

Run Wrangler as a proxy for your local Docusaurus run:

npx wrangler pages dev -- yarn start

For configurable settings in functions we use environment variables. In contrast with Cloudflare Workers, environment variables are not set as globals in your functions, however they can be accessed via handler's context, like that:

// handler function
export async function onRequestPost(context) {
const { request, env } = context;
const apiKey = env.API_KEY;

where API_KEY is the name of environment variable.

For Workers environment variables can be configured in wrangler.toml, but wrangler.toml is not supported by Functions, so the only way to test with environment variables locally is to pass them via command line with -b switch:

npx wrangler pages dev -b API_KEY=123! -b MY_VAR2=some_value ... -- yarn start

For your Cloudflare Pages website, you can configure Production and Preview environment variables on Settings โ†’ Environment variables page:


Environment variables are immutable. If you update/add/delete environment variable and then call the function using it again, it won't work - once variables have changed, the website must be re-built to pick up new values.


Do not put real secrets into "Preview" environment variables if your project in a public repository. Any pull request to the repository publishes "preview" website to a temp URL which is visible to everyone in commit status. Therefore, it's possible for the attacker to submit malicious PR with a function printing all environment variables and then run it via temp URL.

Form submit handlerโ€‹

Email signup form POSTs entered email and hCaptcha response to /api/email-signup function, which performs the following:

  1. Parses request body as JSON and validates its email and captchaToken fields.
  2. Performs hCaptcha response validation and aborts the request if validation fails.
  3. Tries adding a new email (member) into Mailgun mailing list and exits if it's already added.
  4. Sends email with confirmation link via Mailgun to a newly added email address.

Validating hCaptcha responseโ€‹

Validating hCaptcha response on the server is just a POST request to with hCaptcha response received from browser and hCaptcha site key secret in the body:

async function validateCaptcha(token, secret) {
const data = {
response: token,
secret: secret

const encData = urlEncodeObject(data)
const captchaResponse = await fetch(
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': encData.length.toString()
body: encData
const captchaBody = await captchaResponse.json()
if (!captchaBody.success) {
throw captchaBody["error-codes"]

Thanks to this great example on how to send a form request with fetch() method.

Adding email to a mailing listโ€‹

In utils.js we implemented a helper method for calling Mailgun API:

export function callMailgunApi(mailgunApiKey, method, url, data) {
const encData = urlEncodeObject(data)
return fetch(
method: method,
headers: {
Authorization: 'Basic ' + btoa('api:' + mailgunApiKey),
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': encData.length.toString()
body: encData

export function urlEncodeObject(obj) {
return Object.keys(obj)
.map(k => encodeURIComponent(k) + '=' + encodeURIComponent(obj[k]))

Request parameters are passed in URL-encoded form in the body.

Requests require Basic authentication header with api and Mailgun primary account API key as username and password respectively.

With callMailgunApi() helper function adding a new member into Mailgun mailing lists becomes trivial:

async function addMailingListMember(mailgunApiKey, listName, memberAddress) {
const data = {
address: memberAddress,
subscribed: 'no',
upsert: 'no'

const response = await callMailgunApi(mailgunApiKey,
'POST', `${listName}/members`, data)

if (response.status === 200) {
return true; // member has been added
} else if (response.status === 400) {
return false; // member already added
} else {
const responseBody = await response.json()
throw `Error adding mailing list member: ${responseBody.message}`

It tries to add a new member into mailing list and returns true if it was successfully added; otherwise returns false.

Sending confirmation emailโ€‹

The function for sending confirmation email message to a user via Mailgun is just a few lines:

async function sendEmail(mailgunApiKey, mailDomain, from, to, subject, htmlBody) {
const data = {
from: from,
to: to,
subject: subject,
html: htmlBody

const response = await callMailgunApi(mailgunApiKey,
'POST', `${mailDomain}/messages`, data)

if (response.status !== 200) {
const responseBody = await response.text()
throw `Error sending email message: ${responseBody}`

An interesting part here is how confirmation URL is built, which is sent in the message and should be clicked by a user to confirm subscription.

Confirmation URL contains two parameters: email and confirmation code. Email is just recipient's email address which is, obviously, not a secret. Confirmation code is calculated as sha1(email + secret), with secret known to the server only.

When the server receives a request with email and confirmation code, it calculates a new confirmation code for the received email and compares it with the code from the request.

The algorithm could be further improved by implementing expiring confirmation code, but we want to keep it simple for now.

Verifying email and completing signup processโ€‹

/api/confirm-subscription function has a single onRequestGet() handler which performs the following:

  • Validates email and code request parameters.
  • Calculates confirmation code and compares it to the one from the request.
  • If both codes match, updates Mailgun mailing list member's subscribed status to yes.
  • Redirects to a home page with ?signup-confirmed appended to the URL.
export async function onRequestGet(context) {
const { request, env } = context;

// get request params
const { searchParams } = new URL(request.url)
const email = searchParams.get('email')
const code = searchParams.get('code')

if (!code || !email) {
throw "Invalid request parameters"

// validate confirmation code
const calculatedCode = await sha1(email + env.CONFIRM_SECRET)
if (calculatedCode !== code) {
throw "Invalid email or confirmation code"

// update subscription status
await subscribeMailingListMember(env.MAILGUN_API_KEY, env.MAILGUN_MAILING_LIST, email);

// redirect to a home page
return Response.redirect(new URL(request.url).origin + "?signup-confirmed", 302)


In this article we created an email signup form for Docusaurus website protected with hCaptcha. The form allows user to submit their email address and subscribe to a project mailing list. We implemented "double opt-in" process, where upon signup an email is sent to the user which includes a link to click and confirm the subscription. We used Cloudflare Pages Functions to implement all server-side logic. Mailgun service was used to send email messages and maintain mailing list.

In the next article we will build an interactive Python app using Pglet for sending newsletter to Mailgun mailing lists. Make sure to subscribe to Pglet mailing list not to miss it!

ยท 4 min read
Feodor Fitsner

We've just released Pglet v0.5!

While working on this release Pglet made HN home page, got included into TLDR newsletter and topped 500 stars on GitHub (hey, if you enjoy using Pglet give it a โญ๏ธ)! That was exciting - we received great feedback from real users! Many of those comments/requests were incorporated in the release.

Faster WebSocket-based clientsโ€‹

Originally, Pglet was using Redis-like text-based protocol to modify web page controls. There was a proxy process running between a client library and Pglet server translating text commands to WebSocket messages understood by the server. A client library was communitcating with a proxy via Unix named pipes (or named pipes on Windows). Such design was chosen because initially Pglet was made to work with Bash and named pipes is what natively supported there. By adding clients for other languages such as Python and C#/PowerShell it became obvious that the layer of named pipes, command parser/translator and proxy process leads to increased complexity.

With the release of Pglet v0.5, client libraries for Python and C#/PowerShell were re-written to communicate directly with Pglet server via WebSocket protocol. Proxy process is no longer required. As a bonus, when a user application is exiting (CTRL+C), a client library is now sending correct WebSocket closing command to the server, so the app instantly becomes "inactive" and is removed from the server.

Versioning for Pglet and clientsโ€‹

Versions of Pglet Server and its clients follow SemVer format: MAJOR.MINOR.PATCH.

MAJOR part of the version will be changed when Pglet Server API changes break backward compatibility. For now it's 0 meaning there are no drastic changes to the API and it's not mature enough to become 1. All client libraries will follow the same MAJOR number.

Pglet clients could have MINOR version part greater or equal than Pglet server. When the next 0.6 version of Pglet server is released, all client libraries will be bumped to 0.6 as well.

PATCH version part is completely independent for Pglet server and all client libraries.

License changed to Apache 2.0โ€‹

It looks like initially licensing Pglet under AGPL was "premature optimization". Yes, we may want to monetize Pglet with a hosted service and AGPL would stop big cloud providers from forking our service and incorporating it into their cloud offerings, but Pglet is very young project and, honestly, right now we don't foresee Microsoft copying it :)

On the other hand (A)GPL is used to be corporate-unfriendly license which reduces Pglet audience and slows adoption. We want every company using Pglet to build their internal tools!

So, we listened to your feedback and changed Pglet license to Apache License 2.0.

Local mode is now defaultโ€‹

Streaming app UI to the web via hosted Pglet service (aka "Web" mode) is a primary use case for Pglet, so in the previous v0.4 release we made "web" mode default. However, after receiving feedback from users, we realized that an app needs to be tested locally before making it public. Therefore, in Pglet v0.5 we are reverting back "local" mode as default. As before Pglet Server is conveniently bundled into all client libraries to make local app development smooth and pleasant.

We need your feedback!โ€‹

Give Pglet a try and let us know what you think! If you have a question or feature request please submit an issue, start a new discussion or drop an email.

Where does Pglet go next?โ€‹

This is a brief roadmap for Pglet project:

  • PowerShell guide and examples.
  • Node.js/Deno client (it's still based on Pglet v0.4), guide and examples.
  • Go (Golang) client, guide and examples.
  • Pglet hosted service into production:
    • performance optimization,
    • storage cleanup,
    • CPU/memory profiling,
    • metrics,
    • monitoring.

Happy New Year! ๐ŸŽ„๐Ÿค—

ยท 2 min read
Feodor Fitsner

The major feature of Pglet 0.4.6 release is built-in authentication and authorization. Pglet now allows creating protected pages and apps which require users to sign in with any of three methods: GitHub, Google or Microsoft account (Azure AD):

Just imagine, you can instantly add authentication to any of your backend scripts!

For example, in Python to create a page accessible to GitHub user with username ExampleUser and all users in myorg/Developers team:

page ="github:ExampleUser, github:myorg/Developers")

To allow access to specific Azure AD groups/roles you need to specify their full name including tenant ID, for example:

page ="{tenant-guid}/GroupA")

To give access to users authenticating with any method and email domain:

page ="*")

Other changes and improvementsโ€‹

Web mode by defaultโ€‹

Starting from this relase when you start a new page or multi-user app with default parameters its UI will be streamed to To create a local page add local=True parameter (Python), for example:

page ="my-app", local=True) # this page will start a local Pglet server
page.add(Text('Hello, localhost!'))


Pglet hosted service was moved from domain to to emphasize the fact that Pglet is a secure web "console" where your backend apps can output reach progress.

Dark theme added and is now defaultโ€‹

Pglet provides two built-in themes: dark and light and you can configure custom theme. To set page theme change its theme property, for example:

page ="my-app")
page.theme = 'light'

Re-connecting clientโ€‹

A serious stabilization work has been done to make Pglet client more resilient to network fluctuations. Now your Pglet app will stay online for many days, reliably connected to Pglet service.

Python package includes executablesโ€‹

Pglet package now includes pglet executables for all platforms, so they are downloaded during the package installation - that better works for corporate or k8s environments without outbound internet access.

ยท 4 min read
Feodor Fitsner

After publishing this post on Reddit PowerShell community we received great feedback about security. Currently Pglet service is in preview and is not recommended for use in production. We are working on built-in authentication/authorization functionality at the moment. It's going to be "Login with GitHub/Google/Microsoft" OAuth at first plus OpenID for any other providers.

Normally, to access computer via PowerShell you need to configure PowerShell remoting, open WinRM ports on firewall and, the most unpleasant part, add NAT rule on your router to expose a computer to the entire Internet.

So, how can I securely make a web UI for my script without any port opened on the firewall? I used Pglet - a free open-source service for adding remote UI to your scripts. Pglet acts as a relay between your script and a web browser. Your script "dials" the service and sends UI state updates while web users receive live page updates via WebSockets. Kind of Phoenix LiveView for PowerShell :)

To run the app you need to install pglet module:

Install-Module pglet -Scope CurrentUser -Force

The module works on Windows PowerShell on Windows 10 and PowerShell Core 6+ on Windows, Linux and Mac.

Here is how the app looks like:

Below is the entire program (GitHub):

Import-Module pglet

Connect-PgletApp -Name "ps-console/*" -Web -ScriptBlock {
$ErrorActionPreference = 'stop'

$page = $PGLET_PAGE
$page.Title = "PowerShell Remote Console"
$page.HorizontalAlign = 'stretch'

# Textbox with a command entered
$cmd = TextBox -Placeholder "Type PowerShell command and click Run or press ENTER..." -Width '100%'

# Event handler to call when "Run" button is clicked or Enter pressed
$run_on_click = {
$cmd_text = $cmd.value
if ([string]::IsNullOrWhitespace($cmd_text)) {

# disable textbox and Run button, add spinner while the command is evaluating
$cmd.value = ''
$command_panel.disabled = $true
$results.controls.insert(0, (Text $cmd_text -BgColor 'neutralLight' -Padding 10))
$results.controls.insert(1, (Spinner))

try {

# run the command
$result = Invoke-Expression $cmd_text

# if result is Array present it as Grid; otherwise Text
if ($result -is [System.Array]) {
$result_control = Grid -Compact -Items $result
} else {
$result_control = Text -Value ($result | Out-String) -Pre -Padding 10
} catch {
$result_control = Text -Value "$_" -Pre -Padding 10 -Color 'red'

# re-enable controls and replace spinner with the results
$command_panel.disabled = $false
$results.controls.insert(1, $result_control)

# container for command textbox and Run button
$command_panel = Stack -Horizontal -OnSubmit $run_on_click -Controls @(
Button -Text "Run" -Primary -Icon 'Play' -OnClick $run_on_click

# results container
$results = Stack

# "main" view combining all controls together
$view = @(
Stack -Controls @(
Stack -Horizontal -VerticalAlign Center -Controls @(
Text 'Results' -Size large
Button -Icon 'Clear' -Title 'Clear results' -OnClick {

# display the "main" view onto the page

In the program I used just a few Pglet controls: Textbox, Button, Spinner and Stack for the layout. Controls are organized into DOM and every time page.update() is called its state is sent to Pglet service.

When you run the script, a new random URL is generated for your app and printed to the console:

At the very top of the program there is main entry point for the app:

Connect-PgletApp -Name "ps-console/*" -Web -ScriptBlock {

-Name parameter is a page URL and * means a randomly-generated string. You can add your own prefix to the random string or use another namespace, e.g. my-pages/ps-example-*.

If you want to tweak the app and test it locally, remove -Web parameter and a local Pglet server will be started. There is also self-hosted Pglet server if you need more control.

That's it! More great examples are coming!

ยท One min read
Feodor Fitsner

Pglet 0.2.3 adds more chart controls:

  • Pie Chart - demo
  • Line Chart - demo
  • Horizontal Bar Chart - demo

New controlsโ€‹

  • Callout - demo
  • IFrame - demo
  • Header navigation menu (inverted toolbar) - demo

Deep linkingโ€‹

It's now possible to switch between application states (views, areas) by means of URL hash. See how to use it in Deep linking guide.

Other fixes and improvementsโ€‹

  • #13 Deep linking
  • #56 Fixed: VerticalBarChart control: theme colors are not supported in data points
  • #59 iframe control
  • #62 Enhancement - Application Installation Conforms to XDG Specification
  • #65 Line Chart (LineChart) control
  • #67 Horizontal Bar Chart (BarChart) control
  • #69 Pie Chart (PieChart) control
  • #70 Callout control
  • #71 Inverted toolbar button for use in app header
  • #73 Enhancement: configure pglet server options via config file
  • #74 Add host clients authentication with a token
  • #75 Stack control: overflow properties

Give Pglet a try and let us know what you think! There are multiple feedback channels available:

ยท 2 min read
Feodor Fitsner

Pglet 0.2.2 adds the first chart control - VerticalBarChart:

View live demo of VerticalBarChart control

Theming improvementsโ€‹

All color-like attributes in all controls can now accept Fluent UI theme slot colors and "shared" colors.

The list of theme colors can be found on Fluent UI Theme Slots page.

The list of shared colors can be found on Fluent UI Shared Colors page.

For example, you can add an icon with themePrimary color:

add icon name=shop color=themePrimary

Color is being searched in the following order:

  1. Theme slot color
  2. Shared color
  3. Fallback to a named web color or color hex value.

Other fixes and improvementsโ€‹

  • #43 Nav control: unify expanded/collapsed for groups and items
  • #45 ChoiceGroup control: Add "iconColor" property to Option
  • #46 Stack control: add "wrap" property
  • #47 Text control: Markdown mode
  • #48 Chart control: VerticalBarChart
  • #49 Add "trim" attribute to "add" command
  • #52 All color attributes accept theme slot colors and shared colors
  • #53 New control: Image
  • #54 Link control: can contain child controls and other improvements
  • #55 Text control: new "border*" properties

Give Pglet a try and let us know what you think! There are multiple feedback channels available:

ยท 2 min read
Feodor Fitsner

We've just released Pglet 0.2.0!

A ton of new controls were added such as navigation menu, toolbar, grid, tabs, dialog and panel. Now we feel confident Pglet allows to fully unleash your creativity and build a user interface of any complexity!

New featuresโ€‹

ARM supportโ€‹

This release adds binaries for Linux ARM and Apple Silicon M1 - Go 1.16 made that possible. Now you can add remote web UI to your Raspberry PI apps or control what's going on in any IoT device.

Docker imageโ€‹

Docker image pglet/server with self-hosted Pglet Server is now available - run it on a VPS server or drop into your Kubernetes cluster.


Theming in Pglet takes similar approach as in Fluent UI Theme Designer - you choose primary, text, background colors and the theme is auto-magically generated from those colors. To change page theme in Pglet set page control properties: themePrimaryColor, themeTextColor, themeBackgroundColor. Check out Theme demo!

New controlsโ€‹

Improved controlsโ€‹

Button - additional types of Button control were implemented: compound buttons, icon buttons, toolbar, action and link buttons, buttons with context menus and split buttons. Check out Buttons demo!

Text - You can now control the styling of Text control such as color and background as well as border properties and text alignment within it (vertical alignment in center works too!). Check out Text demo!

Other fixes and improvementsโ€‹

  • Controls are based on Fluent UI 8.
  • replace command.
  • Event ticker to avoid hanging event loops.
  • Pglet Server now does not allow remote host clients by default. Remote hosts clients can be enabled with ALLOW_REMOTE_HOST_CLIENTS=true environment variable. Pglet Server Docker image set this variable by default.

Give Pglet a try and let us know what you think! There are multiple feedback channels available:

ยท 4 min read
Feodor Fitsner

Today we are officially launching Pglet!

This is not a groundbreaking event shaking the World and it won't make any ripples on the Internet, but it's still very important milestone for us as it's a good chance to make functionality cut off and present Pglet to the public.

What we've gotโ€‹

You probably won't be able to do a real app with Pglet yet, but we believe it's quite the MVP in a Technical Preview state. The core Pglet functionality is there: pages can be created, controls can be added, modified and removed with live updates streamed to users via WebSockets, page control events triggered by users are broadcasted back to your program - the entire concept's working. We've got basic layout (Stack) and data entry controls (Textbox, Button, etc.) to do simple apps and dashboards, but Fluent UI library, Pglet is based on, is huge and it's a lot more controls to do!

We've got Pglet client bindinds for 4 languages: Python, Bash, PowerShell and Node.js. We chose these languages for MVP to have a good sense of what might be involved in supporting another language. These are scriping non-typed environments mostly, but probably the next binding we do would be Go or C#. Python bindings is the most complete implementation with classes for every control and control event handlers.

We've got Pglet Service which is a hosted Pglet server which you can use right away to bring your web experiences to the web. For technical preview it's just a basic deployment on GAE (will do a separate blog post about that), but quite enough to play with.

The experienceโ€‹

It's been really exciting to work on Pglet during the last 6 months and we learned a lot. Being a C# and mostly Windows developer for more than 15 years it was an absolute pleasure to develop in Go: clean and simple language syntax, goroutines and channels, everything async by design, explicit exceptions management - I'll probably do another post about that experience! Making Pglet UI in React with TypeScript was fun as well: both are fantastic technologies! There is also a challenge to support multiple platforms. Pglet works on Windows, Linux and macOS and you need to constantly think about the experience on all 3 platforms and do a triple amount of tests, CI workflows and other chore things.

What's nextโ€‹

For year 2021 our goal is being able to build and run full-blown backend apps in production. Therefore we are going to work in multiple directions:


We are going to add more controls and improve existing ones. Pglet is still missing navigation controls like menus, toolbars and tabs. Grid is on top priority, for sure, and it's going to be the huge! Charts will be added as well, so you can build beautiful dashboards.

Pglet Serviceโ€‹

This year we are going to bring Pglet Service into production mode with a proper persistence, authentication and account/profile dashboards. All Pglet backend UI is going to be implemented with Pglet.

More docs and examplesโ€‹

We'll be working on providing more Pglet examples, we'll write deployment guides for standalone Pglet apps and self-hosted Pglet Server.


At this stage we are actively looking for any feedback to understand if the project idea is moving in the right direction. We'd be happy to know what would be your requirements, what's missing in Pglet, what's nice or inconvenient, what could be implemented with higher priority.

Feel free to give Pglet a try and let us know what you think! There are multiple feedback channels available:

ยท 5 min read
Feodor Fitsner
Pglet empowers DevOps to easily add rich user interface into their internal apps and utilities without any knowledge of HTML, CSS and JavaScript.

The problems of internal appsโ€‹

Hi, I'm Feodor, the founder of AppVeyor - CI/CD service for software developers.

At AppVeyor, as any other startup, we do a lot of internal apps supporting the core business. Our users don't see those apps. These could be scripts for processing database data, monitoring dashboards, background apps for housekeeping, scheduled scripts for reporting, backend web apps for account management and billing.

Internal apps need User Interface (UI) to present progress/results and grab user input. The simplest form of UI is text console output. Console output can be easily produced from any program or script.

Text output has limitations. It could be hard to present complex structures like hierarhies or visualize the progress of multiple concurrent processes (e.g. copying files in parallel). There is no easy way to fill out the form. Plain text cannot be grouped into collapsible areas. We need rich UI for our internal apps.

Console output can be logged and studied later or you can sit in front of your computer and stare at log "tail". But we want to be mobile. We want to control internal apps from anywhere and any device. We want to share the progress of long-running process with colleagues or send a link to a realt-time dashboard to a manager. Maybe have "Approve" button in the middle of the script to proceed with irreversible actions. We want to collaborate on the results in a real-time. Does it mean we need to build another web app?

Building web apps is hard. Our small team is mostly DevOps. We all do development, deployment and maintenance. We are all good in doing backend coding in different languages: Bash, PowerShell, Python, C#, TypeScript. However, not every team member is a full-stack developer being able to create a web app. Frontend development is a completely different beast: HTTP, TLS, CGI, HTML, CSS, JavaScript, React, Vue, Angular, Svelte, WebPack and so on. Web development today has a steep learning curve.

Building secure web apps is even harder. Internal app works with sensitive company data (databases, keys, credentials, etc.) and presumably hosted in DMZ. You simply can't allow any developer being able to deploy web app with an access to internal resources and available to the whole world.

Pglet to the rescueโ€‹

Let's say you are the developer responsible for deployment and maintenance of backend services and database - DevOp. You mostly work with Golang and use Bash or Python for writing scripts and tools. Your team asked you to implement a simple real-time dashboard with some metrics from backend services. Dashboard should be accessible outside your org.

Should you do a web app? You don't have much experience of writing web apps. Alright, you know the basics of HTML/CSS, you know how to use StackOverflow, but how do you start with the app? Should it be IIS + FastCGI or Flask, plain HTML + jQuery or React, or something else?

Pglet gives you a page, a set of nice-looking controls and the API to arrange those controls on a page and query their state.

Pglet is the tool that hosts virtual pages for you. Think of a page as a "canvas", a "hub", an "app" where both your programs and users collaborate. Programs use an API to control page contents and listen to the events generated by users. Users view page in their browsers and continuosly receive real-time page updates. When in- API is just plain-text commands like "add column A", "add text B to column A", "get the value of textbox C", "wait until button D is clicked" - it's easy to format/parse strings in any programming language.

Bash-like pseudo-code for a simple app greeting user by the name could look like the following:

# create/connect to a page and return its "handle" (named pipe)
$p = (pglet page connect "myapp")

# display entry form
echo 'add row' > $p
echo 'add col id=form to=row' > $p
echo 'add textbox id=yourName to=form' > $p
echo 'add button id=submit to=form' > $p

# listen for events coming from a page
while read eventName eventTarget < "$"
# user fills out the form and clicks "submit" button
if [[ "$eventTarget" == "submit" && "$eventName" == "click" ]]; then

# read textbox value
echo 'get yourName value' > $p
read $yourName < $p

# replace forms contents with the greeting
echo 'clear form' > $p
echo "add text value='Thank you, $yourName!' to=form" > $p

You can build a web app in Bash! No HTML, no templates, no spaghetti code. You don't need to care about the design of your internal app - you get fully-featured controls with "standard" look-n-feel. What you care about is the time you need to deliver the required functionality.


  • Imperatively program UI with commands.
  • Standard controls: layout, data, form. Skins supported.
  • Fast and simple API via named pipes - call from Bash, PowerShell and any other language.
  • Secure by design. Program makes calls to Pglet to update/query UI. Pglet doesn't have access and knows nothing about internal resources located behind the firewall. Pglet keeps no sensitive data such as connection strings, credentials or certificates.
  • Two types of pages can be hosted:
    • Shared page: multiple programs/scripts can connect to the same page and multiple users can view/interact with the same page.
    • App: a new session is created for every connected user; multiple programs/scripts can serve user sessions (load-balancing).