diff --git a/.checkpatch.conf b/.checkpatch.conf
new file mode 100644
index 0000000..bd2a63b
--- /dev/null
+++ b/.checkpatch.conf
@@ -0,0 +1,37 @@
+# Copyright (c) 2023 Zephyr Project members and individual contributors
+# Copyright (c) 2023 Golioth, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
+--emacs
+--summary-file
+--show-types
+--max-line-length=100
+--min-conf-desc-length=1
+--typedefsfile=../deps/zephyr/scripts/checkpatch/typedefsfile
+
+--ignore BRACES
+--ignore PRINTK_WITHOUT_KERN_LEVEL
+--ignore SPLIT_STRING
+--ignore VOLATILE
+--ignore CONFIG_EXPERIMENTAL
+--ignore PREFER_KERNEL_TYPES
+--ignore PREFER_SECTION
+--ignore AVOID_EXTERNS
+--ignore NETWORKING_BLOCK_COMMENT_STYLE
+--ignore DATE_TIME
+--ignore MINMAX
+--ignore CONST_STRUCT
+--ignore FILE_PATH_CHANGES
+--ignore SPDX_LICENSE_TAG
+--ignore C99_COMMENT_TOLERANCE
+--ignore REPEATED_WORD
+--ignore UNDOCUMENTED_DT_STRING
+--ignore DT_SPLIT_BINDING_PATCH
+--ignore DT_SCHEMA_BINDING_PATCH
+--ignore TRAILING_SEMICOLON
+--ignore COMPLEX_MACRO
+--ignore MULTISTATEMENT_MACRO_USE_DO_WHILE
+--ignore ENOSYS
+
+--no-tree
+--exclude patches
diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000..b785676
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,90 @@
+# Copyright (c) 2023 Zephyr Project members and individual contributors
+# Copyright (c) 2023 Golioth, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
+# Copied from Zephyr: https://github.com/zephyrproject-rtos/zephyr/blob/main/.clang-format
+
+# Note: The list of ForEachMacros can be obtained using:
+#
+#    git grep -h '^#define [^[:space:]]*FOR_EACH[^[:space:]]*(' include/ \
+#    | sed "s,^#define \([^[:space:]]*FOR_EACH[^[:space:]]*\)(.*$,  - '\1'," \
+#    | sort | uniq
+#
+# References:
+#   - https://clang.llvm.org/docs/ClangFormatStyleOptions.html
+
+---
+BasedOnStyle: LLVM
+AlignConsecutiveMacros: AcrossComments
+AllowShortBlocksOnASingleLine: false
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortEnumsOnASingleLine: false
+AllowShortFunctionsOnASingleLine: None
+AllowShortIfStatementsOnASingleLine: false
+AllowShortLoopsOnASingleLine: false
+AttributeMacros:
+  - __aligned
+  - __deprecated
+  - __packed
+  - __printf_like
+  - __syscall
+  - __subsystem
+BitFieldColonSpacing: After
+BreakBeforeBraces: Linux
+ColumnLimit: 100
+ConstructorInitializerIndentWidth: 8
+ContinuationIndentWidth: 8
+ForEachMacros:
+  - 'FOR_EACH'
+  - 'FOR_EACH_FIXED_ARG'
+  - 'FOR_EACH_IDX'
+  - 'FOR_EACH_IDX_FIXED_ARG'
+  - 'FOR_EACH_NONEMPTY_TERM'
+  - 'RB_FOR_EACH'
+  - 'RB_FOR_EACH_CONTAINER'
+  - 'SYS_DLIST_FOR_EACH_CONTAINER'
+  - 'SYS_DLIST_FOR_EACH_CONTAINER_SAFE'
+  - 'SYS_DLIST_FOR_EACH_NODE'
+  - 'SYS_DLIST_FOR_EACH_NODE_SAFE'
+  - 'SYS_SFLIST_FOR_EACH_CONTAINER'
+  - 'SYS_SFLIST_FOR_EACH_CONTAINER_SAFE'
+  - 'SYS_SFLIST_FOR_EACH_NODE'
+  - 'SYS_SFLIST_FOR_EACH_NODE_SAFE'
+  - 'SYS_SLIST_FOR_EACH_CONTAINER'
+  - 'SYS_SLIST_FOR_EACH_CONTAINER_SAFE'
+  - 'SYS_SLIST_FOR_EACH_NODE'
+  - 'SYS_SLIST_FOR_EACH_NODE_SAFE'
+  - '_WAIT_Q_FOR_EACH'
+  - 'Z_FOR_EACH'
+  - 'Z_FOR_EACH_ENGINE'
+  - 'Z_FOR_EACH_EXEC'
+  - 'Z_FOR_EACH_FIXED_ARG'
+  - 'Z_FOR_EACH_FIXED_ARG_EXEC'
+  - 'Z_FOR_EACH_IDX'
+  - 'Z_FOR_EACH_IDX_EXEC'
+  - 'Z_FOR_EACH_IDX_FIXED_ARG'
+  - 'Z_FOR_EACH_IDX_FIXED_ARG_EXEC'
+  - 'Z_GENLIST_FOR_EACH_CONTAINER'
+  - 'Z_GENLIST_FOR_EACH_CONTAINER_SAFE'
+  - 'Z_GENLIST_FOR_EACH_NODE'
+  - 'Z_GENLIST_FOR_EACH_NODE_SAFE'
+# Disabled for now, see bug https://github.com/zephyrproject-rtos/zephyr/issues/48520
+#IncludeBlocks: Regroup
+IncludeCategories:
+  - Regex: '^".*\.h"$'
+    Priority: 0
+  - Regex: '^<(assert|complex|ctype|errno|fenv|float|inttypes|limits|locale|math|setjmp|signal|stdarg|stdbool|stddef|stdint|stdio|stdlib|string|tgmath|time|wchar|wctype)\.h>$'
+    Priority: 1
+  - Regex: '^\<zephyr/.*\.h\>$'
+    Priority: 2
+  - Regex: '.*'
+    Priority: 3
+IndentCaseLabels: false
+IndentWidth: 8
+# SpaceBeforeParens: ControlStatementsExceptControlMacros # clang-format >= 13.0
+SortIncludes: false
+UseTab: Always
+WhitespaceSensitiveMacros:
+  - STRINGIFY
+  - Z_STRINGIFY
+  - IF_ENABLED
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..c346a38
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,98 @@
+# Copyright (c) 2023 Zephyr Project members and individual contributors
+# Copyright (c) 2023 Golioth, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
+# Copied from Zephyr: https://github.com/zephyrproject-rtos/zephyr/blob/main/.editorconfig
+
+# EditorConfig: https://editorconfig.org/
+
+# top-most EditorConfig file
+root = true
+
+# All (Defaults)
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+max_line_length = 100
+
+# Assembly
+[*.S]
+indent_style = tab
+indent_size = 8
+
+# C
+[*.{c,h}]
+indent_style = tab
+indent_size = 8
+
+# C++
+[*.{cpp,hpp}]
+indent_style = tab
+indent_size = 8
+
+# Linker Script
+[*.ld]
+indent_style = tab
+indent_size = 8
+
+# Python
+[*.py]
+indent_style = space
+indent_size = 4
+
+# Perl
+[*.pl]
+indent_style = tab
+indent_size = 8
+
+# reStructuredText
+[*.rst]
+indent_style = space
+indent_size = 3
+
+# YAML
+[*.{yml,yaml}]
+indent_style = space
+indent_size = 2
+
+# Shell Script
+[*.sh]
+indent_style = space
+indent_size = 4
+
+# Windows Command Script
+[*.cmd]
+end_of_line = crlf
+indent_style = tab
+indent_size = 8
+
+# Valgrind Suppression File
+[*.supp]
+indent_style = space
+indent_size = 3
+
+# CMake
+[{CMakeLists.txt,*.cmake}]
+indent_style = space
+indent_size = 2
+
+# Makefile
+[Makefile]
+indent_style = tab
+indent_size = 8
+
+# Device tree
+[*.{dts,dtsi,overlay}]
+indent_style = tab
+indent_size = 8
+
+# Git commit messages
+[COMMIT_EDITMSG]
+max_line_length = 75
+
+# Kconfig
+[Kconfig*]
+indent_style = tab
+indent_size = 8
diff --git a/.github/workflows/build_zephyr.yml b/.github/workflows/build_zephyr.yml
new file mode 100644
index 0000000..ce14771
--- /dev/null
+++ b/.github/workflows/build_zephyr.yml
@@ -0,0 +1,82 @@
+# Copyright (c) 2023 Golioth, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
+name: Build Zephyr binaries
+
+on:
+  workflow_dispatch:
+    inputs:
+      ZEPHYR_SDK:
+        required: true
+        type: string
+        default: 0.16.3
+      BOARD:
+        required: true
+        type: string
+        default: nrf52840dk_nrf52840
+      ARTIFACT:
+        required: true
+        type: boolean
+        default: false
+      TAG:
+        type: string
+
+  workflow_call:
+    inputs:
+      ZEPHYR_SDK:
+        required: true
+        type: string
+      BOARD:
+        required: true
+        type: string
+      ARTIFACT:
+        required: true
+        type: boolean
+      TAG:
+        type: string
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+
+    container: golioth/golioth-zephyr-base:${{ inputs.ZEPHYR_SDK }}-SDK-v0
+
+    env:
+      ZEPHYR_SDK_INSTALL_DIR: /opt/toolchains/zephyr-sdk-${{ inputs.ZEPHYR_SDK }}
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+        with:
+          path: app
+      - name: Setup West workspace
+        run: |
+          west init -l app
+          west update --narrow -o=--depth=1
+          west zephyr-export
+          pip3 install -r deps/zephyr/scripts/requirements-base.txt
+          # Needed for TF-M
+          pip3 install cryptography pyasn1 pyyaml cbor>=1.0.0 imgtool>=1.9.0 jinja2 click
+
+      - name: Build with West
+        run: |
+          west build -p -b ${{ inputs.BOARD }} app
+
+      - name: Prepare artifacts
+        if: inputs.ARTIFACT == true && inputs.TAG != ''
+
+        run: |
+          cd build/zephyr
+          mkdir -p artifacts
+          mv merged.hex     ./artifacts/golioth-${{ github.event.repository.name }}_${{ inputs.TAG }}_${{ inputs.BOARD }}_full.hex
+          mv app_update.bin ./artifacts/golioth-${{ github.event.repository.name }}_${{ inputs.TAG }}_${{ inputs.BOARD }}_update.bin
+          mv zephyr.elf     ./artifacts/golioth-${{ github.event.repository.name }}_${{ inputs.TAG }}_${{ inputs.BOARD }}.elf
+
+      # Run IDs are unique per repo but are reused on re-runs
+      - name: Save artifact
+        if: inputs.ARTIFACT == true
+        uses: actions/upload-artifact@v3
+        with:
+          name: build_artifacts_${{ github.run_id }}
+          path: |
+            build/zephyr/artifacts/*
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..1433f8c
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,52 @@
+# Copyright (c) 2023 Golioth, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
+name: Create Release
+
+on:
+    workflow_dispatch:
+        inputs:
+            version:
+                description: 'Release Version.'
+                required: true
+                default: 'v0.0.0'
+                type: string
+
+jobs:
+    build-binaries:
+        strategy:
+            matrix:
+              ZEPHYR_SDK: [0.16.3]
+              BOARD: ["nrf52840dk_nrf52840","adafruit_feather_nrf52840"]
+
+        uses: ./.github/workflows/build_zephyr.yml
+        with:
+          ZEPHYR_SDK: ${{ matrix.ZEPHYR_SDK }}
+          BOARD: ${{ matrix.BOARD }}
+          ARTIFACT: true
+          TAG: ${{ inputs.version }}
+
+    upload-binaries:
+        needs: build-binaries
+
+        runs-on: ubuntu-latest
+
+        steps:
+            - name: Checkout repo
+              uses: actions/checkout@v4
+
+            - name: Download artifact
+              uses: actions/download-artifact@v3
+              with:
+                name: build_artifacts_${{ github.run_id }}
+                path: ~/artifacts
+
+            - name: Create Release manually with GH CLI
+              run: gh release create --title ${{ inputs.version }} --draft ${{ inputs.version }}
+              env:
+                GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+            - name: Upload artifacts to release
+              run: gh release upload --clobber ${{ inputs.version }} ~/artifacts/*.*
+              env:
+                GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..2329f00
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,17 @@
+# Copyright (c) 2023 Golioth, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
+name: Test firmware
+
+on:
+  pull_request:
+
+  push:
+
+jobs:
+  test_build:
+    uses: ./.github/workflows/build_zephyr.yml
+    with:
+      ZEPHYR_SDK: 0.16.3
+      BOARD: nrf52840dk_nrf52840
+      ARTIFACT: false
diff --git a/.gitignore b/.gitignore
index fbd2cdd..3938b60 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,8 @@
-deps/
-build/
-.vscode/
+# Copyright (c) 2023 Golioth, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
+build*/
+.vscode
+.cache
 credentials.conf
+__pycache__/
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..538d455
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,18 @@
+<!-- Copyright (c) 2023 Golioth, Inc. -->
+<!-- SPDX-License-Identifier: Apache-2.0 -->
+
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [1.0.0] - TBA
+
+### Added
+- Use of comercialy available GL-S200 Thread Border Router
+- Added support for Adafruit Feather nRF52840 Express board
+- Based on the [Reference Design Template](https://github.com/golioth/reference-design-template)
+- Added a CHANGELOG.md to track changes moving forward.
+
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 06b3172..e2d93bb 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,16 +1,16 @@
-#
-# Copyright (c) 2020 Nordic Semiconductor ASA
-#
-# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
-#
+# Copyright (c) 2022-2023 Golioth, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
 cmake_minimum_required(VERSION 3.20.0)
 
 find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
 
 project(openthread)
 
-# NORDIC SDK APP START
 target_sources(app PRIVATE src/main.c)
-# NORDIC SDK APP END
+target_sources(app PRIVATE src/app_rpc.c)
+target_sources(app PRIVATE src/app_settings.c)
+target_sources(app PRIVATE src/app_state.c)
+target_sources(app PRIVATE src/app_sensors.c)
 
-target_sources_ifdef(CONFIG_BT_NUS app PRIVATE src/ble_utils.c)
+add_subdirectory_ifdef(CONFIG_ALUDEL_BATTERY_MONITOR src/battery_monitor)
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index dd41a2b..b325a7b 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -1,3 +1,6 @@
+<!-- Copyright (c) 2023 Golioth, Inc. -->
+<!-- SPDX-License-Identifier: Apache-2.0 -->
+
 # Contributor Covenant Code of Conduct
 
 ## Our Pledge
diff --git a/Kconfig b/Kconfig
index 27938be..da365e1 100644
--- a/Kconfig
+++ b/Kconfig
@@ -1,13 +1,16 @@
-#
-# Copyright (c) 2020 Nordic Semiconductor ASA
-#
-# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
-#
+# Copyright (c) 2022-2023 Golioth, Inc.
+# SPDX-License-Identifier: Apache-2.0
 
-menu "Zephyr Kernel"
-source "Kconfig.zephyr"
-endmenu
+mainmenu "Golioth application options"
+
+if DNS_RESOLVER
+
+config DNS_SERVER_IP_ADDRESSES
+	default y
 
-module = GOLIOTH_THREAD
-module-str = Golioth Thread
-source "${ZEPHYR_BASE}/subsys/logging/Kconfig.template.log_config"
+config DNS_SERVER1
+	default "1.1.1.1"
+
+endif # DNS_RESOLVER
+
+source "Kconfig.zephyr"
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/README.rst b/README.rst
index 901736d..c4ac4a5 100644
--- a/README.rst
+++ b/README.rst
@@ -1,58 +1,68 @@
-Golioth OpenThread Demo
-#########################
+..
+   Copyright (c) 2022-2024 Golioth, Inc.
+   SPDX-License-Identifier: Apache-2.0
 
-Overview
-********
+Golioth OpenThread Demo
+#######################
 
-A demonstration of a Zephyr device connecting to Golioth over IPv6 via Thread Protocol.
+This repository contains the firmware source code and `pre-built release
+firmware images <releases_>`_ for the Golioth OpenThread Demo.
 
-This is a standalone repository that will download all required files and dependencies. This directory will take up a good amount of room, as it will contain the latest Nordic NCS in the ``deps`` repository..
+The full project details are available on the `Golioth Thread Demo Project Page`_.
 
 
 Requirements
 ************
 
 - Golioth device PSK credentials
-- A running Thread Border Router with NAT64 (we will be using an OpenThread Border Router - OTBR)
-- Thread network name and PSK key
-- `The Laird BT510 <https://www.lairdconnect.com/iot-devices/iot-sensors/bt510-bluetooth-5-long-range-ip67-multi-sensor>`_.  
-
-This demo can also work on the `Nordic nRF52840-DK <https://www.nordicsemi.com/Products/Development-hardware/nrf52840-dk>`_, but will not have on-board sensors. All build commands will explicitly call out the BT510.
-
-There are additional instructions around setting up RCP and OTBR on `our documentation page <https://golioth.github.io/golioth-openthread-demo-docs>`_.
+- A running Thread Border Router with NAT64 translation (we will be using the
+  commercially available off-the-shelf `GL-S200 Thread Border Router`_)
+- Thread Network Name and Network Key
 
-Usage
-*****
+Supported Hardware
+******************
 
-The firmware will wait for a serial console to attach over USB.
+This firmware can be built for a variety of supported hardware platforms.
 
-Once USB is attached, it will proceed with connecting to the pre-configured
-thread network.
+.. pull-quote::
+   [!IMPORTANT]
 
-Upon successful connection, the green LED of the nRF52840 dongle will turn on.
+   In Zephyr, each of these different hardware variants is given a unique
+   "board" identifier, which is used by the build system to generate firmware
+   for that variant.
 
-When you press a button, a new log event will be sent to the Golioth Log device service.
+   When building firmware using the instructions below, make sure to use the
+   correct Zephyr board identifier that corresponds to your hardware platform.
 
-A full Zephyr shell is available on the USB serial console, along with openthread commands.
+.. list-table:: **Nordic Semiconductor Hardware**
+   :header-rows: 1
 
-You can list available openthread commands by running ``ot help`` on the console.
+   * - Development Borad
+     - Zephyr Board
 
+   * - .. image:: images/nRF52840_DK.png
+          :width: 240
+     - ``nrf52840dk_nrf52840``
 
-Download This Repository
-************************
+.. list-table:: **Adafruit Hardware**
+   :header-rows: 1
 
-.. code-block:: console
+   * - Development Board
+     - Zephyr Board
 
-    west init -m https://github.com/golioth/golioth-openthread-demo.git golioth-openthread
-    cd golioth-openthread
-    west update
-    
+   * - .. image:: images/Adafurit_nRF52840_Feather.png
+          :width: 240
+     - ``adafruit_feather_nrf52840``
 
+Firmware Overview
+*****************
+This is a Reference Design for a Thread Protocol enabled device using Zephyr
+and connecting to Golioth over IPv6.
 
 Configure ``prj.conf``
-********************
+======================
 
-Configure the following Kconfig options
+Configure the following Kconfig options:
 
 - OPENTHREAD_NETWORK_NAME       - Name of your Thread network
 - OPENTHREAD_NETWORKKEY         - Network Key of your Thread network
@@ -62,45 +72,209 @@ by changing these lines in the ``prj.conf`` configuration file, e.g.:
 .. code-block:: cfg
 
    CONFIG_OPENTHREAD_NETWORKKEY="00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff"
-   CONFIG_OPENTHREAD_NETWORK_NAME="OpenThreadDemo"
+   CONFIG_OPENTHREAD_NETWORK_NAME="golioth-thread"
 
-Build, Flash, Provision
-***********************
+``CONFIG_OPENTHREAD_CHANNEL`` is set to ``26``.
 
-Build and Flash
-===============
+.. pull-quote::
+   [!IMPORTANT]
 
-.. code-block:: console
-    
-    west build -b bt510 app
-    west flash
+   Make sure the Thread Network Name, Thread Network Key and Thread Channel
+   match your Border Router configuration.
 
-Note, this requires a board with a debugger, either on-board or on an external platform. 
+Supported Golioth Zephyr SDK Features
+=====================================
 
-For the Laird BT510, you will need the `Laird SWD USB programming kit <https://www.lairdconnect.com/wireless-modules/programming-kits/usb-swd-programming-kit>`_.
+This firmware implements the following features from the Golioth Zephyr SDK:
 
-Provision
-=========
+- `Device Settings Service <https://docs.golioth.io/firmware/zephyr-device-sdk/device-settings-service>`_
+- `LightDB State Client <https://docs.golioth.io/firmware/zephyr-device-sdk/light-db/>`_
+- `LightDB Stream Client <https://docs.golioth.io/firmware/zephyr-device-sdk/light-db-stream/>`_
+- `Logging Client <https://docs.golioth.io/firmware/zephyr-device-sdk/logging/>`_
+- `Over-the-Air (OTA) Firmware Upgrade <https://docs.golioth.io/firmware/device-sdk/firmware-upgrade>`_
+- `Remote Procedure Call (RPC) <https://docs.golioth.io/firmware/zephyr-device-sdk/remote-procedure-call>`_
 
-If your device is not connecting to your OpenThread network using the info in your ``prj.conf``, use the following commands on the shell (connect to the device using the programmer)
+Device Settings Service
+-----------------------
 
-.. code-block:: console
-    
-    uart:~$ ot ifconfig down
-    uart:~$ ot dataset networkkey 00112233445566778899aabbccddeeff
-    uart:~$ ot dataset networkname OpenThreadDemo
-    uart:~$ ot dataset commit active
-    uart:~$ ot ifconfig up
-    uart:~$ ot thread start
+The following settings should be set in the Device Settings menu of the
+`Golioth Console`_.
+
+``LOOP_DELAY_S``
+   Adjusts the delay between sensor readings. Set to an integer value (seconds).
+
+   Default value is ``60`` seconds.
+
+LightDB Stream Service
+----------------------
+
+An up-counting timer is periodically sent to the ``sensor/counter`` endpoint of the
+LightDB Stream service to simulate sensor data.
+
+LightDB State Service
+---------------------
+
+The concept of Digital Twin is demonstrated with the LightDB State
+``example_int0`` and ``example_int1`` variables that are members of the ``desired``
+and ``state`` endpoints.
+
+* ``desired`` values may be changed from the cloud side. The device will recognize
+  these, validate them for [0..65535] bounding, and then reset these endpoints
+  to ``-1``
 
-Check your device is attempting to attach to the OTBR using the command ``ot state``
+* ``state`` values will be updated by the device whenever a valid value is
+  received from the ``desired`` endpoints. The cloud may read the ``state``
+  endpoints to determine device status, but only the device should ever write to
+  the ``state`` endpoints.
 
-Finally, add your Golioth credentials using the settings shell. Connect over serial (programmer) to your device and then apply your Golioth PSK-ID / PSK
+Remote Procedure Call (RPC) Service
+-----------------------------------
+
+The following RPCs can be initiated in the Remote Procedure Call menu of the
+`Golioth Console`_.
+
+``reboot``
+   Reboot the system.
+
+``set_log_level``
+   Set the log level.
+
+   The method takes a single parameter which can be one of the following integer
+   values:
+
+   * ``0``: ``LOG_LEVEL_NONE``
+   * ``1``: ``LOG_LEVEL_ERR``
+   * ``2``: ``LOG_LEVEL_WRN``
+   * ``3``: ``LOG_LEVEL_INF``
+   * ``4``: ``LOG_LEVEL_DBG``
+
+Local set up
+************
+
+Do not clone this repo using git. Zephyr's ``west`` meta tool should be used to
+set up your local workspace.
+
+Install the Python virtual environment (recommended)
+====================================================
+
+.. code-block:: shell
+
+   cd ~
+   mkdir golioth-openthread-demo
+   python -m venv golioth-openthread-demo/.venv
+   source golioth-openthread-demo/.venv/bin/activate
+   pip install wheel west
+
+Use ``west`` to initialize the workspace and install dependencies
+=================================================================
 
 .. code-block:: console
-    
-    uart:~$ settings set golioth/psk-id <my-psk-id@my-project>
-    uart:~$ settings set golioth/psk <my-psk>
-    uart:~$ kernel reboot cold
 
-These will persist after updates to your firmware, so you should only need to add them once.
\ No newline at end of file
+   cd ~/golioth-openthread-demo
+   west init -m git@github.com:golioth/golioth-openthread-demo.git .
+   west update
+   west zephyr-export
+   pip install -r deps/zephyr/scripts/requirements.txt
+
+Building the application
+************************
+
+Build the Zephyr sample application from the top-level workspace of your project.
+After a successful build you will see a new ``build/`` directory.
+
+Note that this git repository was cloned into the ``app`` folder, so any changes
+you make to the application itself should be committed inside this repository.
+The ``build`` and ``deps`` directories in the root of the workspace are managed
+outside of this git repository by the ``west`` meta-tool.
+
+Prior to building, update ``CONFIG_MCUBOOT_IMGTOOL_SIGN_VERSION`` in the ``prj.conf`` file to
+reflect the firmware version number you want to assign to this build. Then run the following
+commands to build and program the firmware onto the device.
+
+.. pull-quote::
+   [!IMPORTANT]
+
+   When running the commands below, make sure to replace the placeholder
+   ``<your_zephyr_board_id>`` with the actual Zephyr board from the table above
+   that matches your hardware.
+
+   In addition, replace ``<your.semantic.version>`` with a `SemVer`_-compliant
+   version string (e.g. ``1.2.3``) that will be used by the DFU service when
+   checking for firmware updates.
+
+.. code-block:: text
+
+   $ (.venv) west build -p -b <your_zephyr_board_id> app
+
+For example, to build firmware for the `Nordic nRF52840 DK`_-based follow-along hardware:
+
+.. code-block:: text
+
+   $ (.venv) west build -p -b nrf52840dk_nrf52840 app
+
+Flash the firmware
+==================
+
+.. code-block:: text
+
+   $ (.venv) west flash
+
+Provision the device
+====================
+
+In order for the device to securely authenticate with the Golioth Cloud, we need
+to provision the device with a pre-shared key (PSK). This key will persist
+across reboots and only needs to be set once after the device firmware has been
+programmed. In addition, flashing new firmware images with ``west flash`` should
+not erase these stored settings unless the entire device flash is erased.
+
+Configure the PSK-ID and PSK using the device UART shell and reboot the device:
+
+.. code-block:: text
+
+   uart:~$ settings set golioth/psk-id <my-psk-id@my-project>
+   uart:~$ settings set golioth/psk <my-psk>
+   uart:~$ kernel reboot cold
+
+
+Pulling in updates from the Reference Design Template
+*****************************************************
+
+This reference design was forked from the `Reference Design Template`_ repo. We
+recommend the following workflow to pull in future changes:
+
+* Setup
+
+  * Create a ``template`` remote based on the Reference Design Template
+    repository
+
+* Merge in template changes
+
+  * Fetch template changes and tags
+  * Merge template release tag into your ``main`` (or other branch)
+  * Resolve merge conflicts (if any) and commit to your repository
+
+.. code-block:: shell
+
+   # Setup
+   git remote add template https://github.com/golioth/reference-design-template.git
+   git fetch template --tags
+
+   # Merge in template changes
+   git fetch template --tags
+   git checkout your_local_branch
+   git merge template_v1.0.0
+
+   # Resolve merge conflicts if necessary
+   git add resolved_files
+   git commit
+
+.. _Golioth Console: https://console.golioth.io
+.. _GL-S200 Thread Border Router: https://www.gl-inet.com/products/gl-s200/
+.. _Nordic nRF52840 DK: https://www.nordicsemi.com/Products/Development-hardware/nRF52840-DK
+.. _Golioth Thread Demo Project Page: https://projects.golioth.io/reference-designs/openthread-demo/
+.. _releases: https://github.com/golioth/
+.. _Zephyr Getting Started Guide: https://docs.zephyrproject.org/latest/develop/getting_started/
+.. _Developer Training: https://training.golioth.io
+.. _SemVer: https://semver.org
+.. _Reference Design Template: https://github.com/golioth/reference-design-template
diff --git a/boards/adafruit_feather_nrf52840.conf b/boards/adafruit_feather_nrf52840.conf
new file mode 100644
index 0000000..a8a564a
--- /dev/null
+++ b/boards/adafruit_feather_nrf52840.conf
@@ -0,0 +1,48 @@
+# Copyright (c) 2024 Golioth, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
+CONFIG_LOG=y
+
+CONFIG_NETWORKING=y
+CONFIG_NET_L2_OPENTHREAD=y
+CONFIG_MPSL=y
+
+CONFIG_NET_SHELL=y
+CONFIG_OPENTHREAD_SHELL=y
+CONFIG_SHELL_ARGC_MAX=26
+CONFIG_SHELL_CMD_BUFF_SIZE=416
+
+CONFIG_MBEDTLS_SHA1_C=n
+CONFIG_FPU=y
+
+# TLS configuration
+CONFIG_MBEDTLS=y
+CONFIG_MBEDTLS_BUILTIN=n
+CONFIG_MBEDTLS_ENABLE_HEAP=y
+CONFIG_MBEDTLS_HEAP_SIZE=10240
+
+# PSK needs to be manually enabled to prevent ENOTSUP (-134)
+CONFIG_MBEDTLS_KEY_EXCHANGE_SOME_PSK_ENABLED=y
+CONFIG_MBEDTLS_KEY_EXCHANGE_PSK_ENABLED=y
+CONFIG_NET_SOCKETS_SOCKOPT_TLS=y
+
+CONFIG_THREAD_NAME=y
+CONFIG_SCHED_CPU_MASK=y
+CONFIG_THREAD_ANALYZER=y
+
+# Shell setup
+CONFIG_SHELL=y
+
+# Settings shell
+CONFIG_SETTINGS=y
+CONFIG_GOLIOTH_SETTINGS=y
+CONFIG_SETTINGS_RUNTIME=y
+CONFIG_GOLIOTH_SAMPLE_PSK_SETTINGS=y
+CONFIG_GOLIOTH_SAMPLE_SETTINGS_AUTOLOAD=y
+CONFIG_GOLIOTH_SAMPLE_SETTINGS_SHELL=y
+
+CONFIG_FLASH=y
+CONFIG_FLASH_MAP=y
+CONFIG_NVS=y
+
+CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048
diff --git a/boards/adafruit_feather_nrf52840.overlay b/boards/adafruit_feather_nrf52840.overlay
new file mode 100644
index 0000000..bd60592
--- /dev/null
+++ b/boards/adafruit_feather_nrf52840.overlay
@@ -0,0 +1,21 @@
+/* Copyright (c) 2020 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+/ {
+	chosen {
+		zephyr,entropy = &rng;
+		nordic,pm-ext-flash = &gd25q16;
+	};
+
+	aliases {
+		golioth-led = &led1;
+		user-btn = &button0;
+	};
+};
+
+&uart0 {
+	status = "okay";
+};
+
diff --git a/boards/nrf21540dk_nrf52840.overlay b/boards/nrf21540dk_nrf52840.overlay
deleted file mode 100644
index a1fb0de..0000000
--- a/boards/nrf21540dk_nrf52840.overlay
+++ /dev/null
@@ -1,48 +0,0 @@
-/* Copyright (c) 2020 Nordic Semiconductor ASA
- *
- * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
- */
-
-/* Default Zephyr configuration already provides GPIO support for FEM. */
-
-/ {
-	/*
-	* In some default configurations within the nRF Connect SDK,
-	* e.g. on nRF52840, the chosen zephyr,entropy node is &cryptocell.
-	* This devicetree overlay ensures that default is overridden wherever it
-	* is set, as this application uses the RNG node for entropy exclusively.
-	*/
-	chosen {
-		zephyr,entropy = &rng;
-	};
-};
-&adc {
-	status = "disabled";
-};
-&uart1 {
-	status = "disabled";
-};
-&pwm0 {
-	status = "disabled";
-};
-&i2c0 {
-	status = "disabled";
-};
-&spi0 {
-	status = "disabled";
-};
-&spi1 {
-	status = "disabled";
-};
-&spi2 {
-	status = "disabled";
-};
-&spi3 {
-	status = "disabled";
-};
-&qspi {
-	status = "disabled";
-};
-&usbd {
-	status = "disabled";
-};
diff --git a/boards/nrf52833dk_nrf52833.overlay b/boards/nrf52833dk_nrf52833.overlay
deleted file mode 100644
index 7a654d8..0000000
--- a/boards/nrf52833dk_nrf52833.overlay
+++ /dev/null
@@ -1,36 +0,0 @@
-/* Copyright (c) 2021 Nordic Semiconductor ASA
- *
- * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
- */
-
-
-&adc {
-	status = "disabled";
-};
-&uart1 {
-	status = "disabled";
-};
-&pwm0 {
-	status = "disabled";
-};
-&i2c0 {
-	status = "disabled";
-};
-&spi0 {
-	status = "disabled";
-};
-&spi1 {
-	status = "disabled";
-};
-&spi2 {
-	status = "disabled";
-};
-&spi3 {
-	status = "disabled";
-};
-&usbd {
-	status = "disabled";
-};
-&gpio1 {
-	status = "disabled";
-};
diff --git a/boards/nrf52840dk_nrf52840.conf b/boards/nrf52840dk_nrf52840.conf
new file mode 100644
index 0000000..a8a564a
--- /dev/null
+++ b/boards/nrf52840dk_nrf52840.conf
@@ -0,0 +1,48 @@
+# Copyright (c) 2024 Golioth, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
+CONFIG_LOG=y
+
+CONFIG_NETWORKING=y
+CONFIG_NET_L2_OPENTHREAD=y
+CONFIG_MPSL=y
+
+CONFIG_NET_SHELL=y
+CONFIG_OPENTHREAD_SHELL=y
+CONFIG_SHELL_ARGC_MAX=26
+CONFIG_SHELL_CMD_BUFF_SIZE=416
+
+CONFIG_MBEDTLS_SHA1_C=n
+CONFIG_FPU=y
+
+# TLS configuration
+CONFIG_MBEDTLS=y
+CONFIG_MBEDTLS_BUILTIN=n
+CONFIG_MBEDTLS_ENABLE_HEAP=y
+CONFIG_MBEDTLS_HEAP_SIZE=10240
+
+# PSK needs to be manually enabled to prevent ENOTSUP (-134)
+CONFIG_MBEDTLS_KEY_EXCHANGE_SOME_PSK_ENABLED=y
+CONFIG_MBEDTLS_KEY_EXCHANGE_PSK_ENABLED=y
+CONFIG_NET_SOCKETS_SOCKOPT_TLS=y
+
+CONFIG_THREAD_NAME=y
+CONFIG_SCHED_CPU_MASK=y
+CONFIG_THREAD_ANALYZER=y
+
+# Shell setup
+CONFIG_SHELL=y
+
+# Settings shell
+CONFIG_SETTINGS=y
+CONFIG_GOLIOTH_SETTINGS=y
+CONFIG_SETTINGS_RUNTIME=y
+CONFIG_GOLIOTH_SAMPLE_PSK_SETTINGS=y
+CONFIG_GOLIOTH_SAMPLE_SETTINGS_AUTOLOAD=y
+CONFIG_GOLIOTH_SAMPLE_SETTINGS_SHELL=y
+
+CONFIG_FLASH=y
+CONFIG_FLASH_MAP=y
+CONFIG_NVS=y
+
+CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048
diff --git a/boards/nrf52840dk_nrf52840.overlay b/boards/nrf52840dk_nrf52840.overlay
index 166cb29..8f74714 100644
--- a/boards/nrf52840dk_nrf52840.overlay
+++ b/boards/nrf52840dk_nrf52840.overlay
@@ -4,46 +4,19 @@
  */
 
 / {
-	/*
-	* In some default configurations within the nRF Connect SDK,
-	* e.g. on nRF52840, the chosen zephyr,entropy node is &cryptocell.
-	* This devicetree overlay ensures that default is overridden wherever it
-	* is set, as this application uses the RNG node for entropy exclusively.
-	*/
 	chosen {
 		zephyr,entropy = &rng;
+		nordic,pm-ext-flash = &mx25r64;
+	};
+
+	aliases {
+		golioth-led = &led1;
+		user-btn = &button0;
 	};
 };
-&adc {
-	status = "disabled";
-};
-&uart1 {
-	status = "disabled";
-};
-&pwm0 {
-	status = "disabled";
-};
-&i2c0 {
-	status = "disabled";
-};
-&spi0 {
-	status = "disabled";
-};
-&spi1 {
-	status = "disabled";
-};
-&spi2 {
-	status = "disabled";
-};
-&spi3 {
-	status = "disabled";
-};
-&qspi {
-	status = "disabled";
-};
-&usbd {
-	status = "disabled";
-};
-&gpio1 {
-	status = "disabled";
+
+&uart0 {
+	status = "okay";
 };
+
+
diff --git a/boards/nrf5340dk_nrf5340_cpuapp.conf b/boards/nrf5340dk_nrf5340_cpuapp.conf
deleted file mode 100644
index bb2ff81..0000000
--- a/boards/nrf5340dk_nrf5340_cpuapp.conf
+++ /dev/null
@@ -1,10 +0,0 @@
-#
-# Copyright (c) 2021 Nordic Semiconductor
-#
-# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
-#
-
-# Default PRNG entropy for nRF53 Series devices is CSPRNG CC312
-# which for that purpose is too slow yet
-# Use Xoroshiro128+ as PRNG
-CONFIG_XOROSHIRO_RANDOM_GENERATOR=y
diff --git a/boards/nrf5340dk_nrf5340_cpuapp_ns.conf b/boards/nrf5340dk_nrf5340_cpuapp_ns.conf
deleted file mode 100644
index 6c0ba83..0000000
--- a/boards/nrf5340dk_nrf5340_cpuapp_ns.conf
+++ /dev/null
@@ -1,9 +0,0 @@
-#
-# Copyright (c) 2021 Nordic Semiconductor
-#
-# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
-#
-
-# Enable Trusted Firmware M
-CONFIG_BUILD_WITH_TFM=y
-CONFIG_OPENTHREAD_CRYPTO_PSA=y
diff --git a/child_image/mcuboot/boards/adafruit_feather_nrf52840.conf b/child_image/mcuboot/boards/adafruit_feather_nrf52840.conf
new file mode 100644
index 0000000..4a03a13
--- /dev/null
+++ b/child_image/mcuboot/boards/adafruit_feather_nrf52840.conf
@@ -0,0 +1,24 @@
+#
+# Copyright (c) 2021 Nordic Semiconductor ASA
+#
+# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+#
+
+# In order to provide board specific configurations to the MCUboot child image
+# we also need to provide a base configuration for MCUboot. This file contains
+# the basic configurations needed to successfully build and run MCUboot.
+
+# MCUboot requires a large stack size, otherwise an MPU fault will occur
+CONFIG_MAIN_STACK_SIZE=10240
+
+# Enable flash operations
+CONFIG_FLASH=y
+
+# This must be increased to accommodate the bigger images.
+CONFIG_BOOT_MAX_IMG_SECTORS=256
+
+# Enable serial recovery
+CONFIG_UART_CONSOLE=n
+CONFIG_MCUBOOT_SERIAL=y
+CONFIG_MCUBOOT_SERIAL_DIRECT_IMAGE_UPLOAD=y
+
diff --git a/child_image/mcuboot/boards/adafruit_feather_nrf52840.overlay b/child_image/mcuboot/boards/adafruit_feather_nrf52840.overlay
new file mode 100644
index 0000000..c5f2f3a
--- /dev/null
+++ b/child_image/mcuboot/boards/adafruit_feather_nrf52840.overlay
@@ -0,0 +1,17 @@
+/* Copyright (c) 2020 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+/ {
+	chosen {
+		nordic,pm-ext-flash = &gd25q16;
+	};
+
+
+	aliases {
+		mcuboot-button0 = &button0;
+	};
+};
+
+
diff --git a/child_image/mcuboot/boards/nrf52840dk_nrf52840.conf b/child_image/mcuboot/boards/nrf52840dk_nrf52840.conf
new file mode 100644
index 0000000..4a03a13
--- /dev/null
+++ b/child_image/mcuboot/boards/nrf52840dk_nrf52840.conf
@@ -0,0 +1,24 @@
+#
+# Copyright (c) 2021 Nordic Semiconductor ASA
+#
+# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+#
+
+# In order to provide board specific configurations to the MCUboot child image
+# we also need to provide a base configuration for MCUboot. This file contains
+# the basic configurations needed to successfully build and run MCUboot.
+
+# MCUboot requires a large stack size, otherwise an MPU fault will occur
+CONFIG_MAIN_STACK_SIZE=10240
+
+# Enable flash operations
+CONFIG_FLASH=y
+
+# This must be increased to accommodate the bigger images.
+CONFIG_BOOT_MAX_IMG_SECTORS=256
+
+# Enable serial recovery
+CONFIG_UART_CONSOLE=n
+CONFIG_MCUBOOT_SERIAL=y
+CONFIG_MCUBOOT_SERIAL_DIRECT_IMAGE_UPLOAD=y
+
diff --git a/child_image/mcuboot/boards/nrf52840dk_nrf52840.overlay b/child_image/mcuboot/boards/nrf52840dk_nrf52840.overlay
new file mode 100644
index 0000000..82e11a6
--- /dev/null
+++ b/child_image/mcuboot/boards/nrf52840dk_nrf52840.overlay
@@ -0,0 +1,11 @@
+/* Copyright (c) 2020 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+/ {
+	chosen {
+		nordic,pm-ext-flash = &mx25r64;
+	};
+};
+
diff --git a/images/Adafurit_nRF52840_Feather.png b/images/Adafurit_nRF52840_Feather.png
new file mode 100644
index 0000000..a4f94d7
Binary files /dev/null and b/images/Adafurit_nRF52840_Feather.png differ
diff --git a/images/nRF52840_DK.png b/images/nRF52840_DK.png
new file mode 100644
index 0000000..6547dd0
Binary files /dev/null and b/images/nRF52840_DK.png differ
diff --git a/overlay-logging.conf b/overlay-logging.conf
deleted file mode 100644
index 4bb5ff2..0000000
--- a/overlay-logging.conf
+++ /dev/null
@@ -1,22 +0,0 @@
-#
-# Copyright (c) 2022 Nordic Semiconductor
-#
-# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
-
-# Enable net module logging
-# CONFIG_NET_LOG=y
-# Option for configuring log level in net config library
-# CONFIG_NET_CONFIG_LOG_LEVEL_INF=y
-
-# Option for configuring log level in Zephyr L2 logging
-# CONFIG_OPENTHREAD_L2_DEBUG=y
-# CONFIG_OPENTHREAD_L2_LOG_LEVEL_DBG=y
-# CONFIG_OPENTHREAD_L2_DEBUG_DUMP_15_4=y
-# CONFIG_OPENTHREAD_L2_DEBUG_DUMP_IPV6=y
-
-# Option for configuring log level in OpenThread
-CONFIG_OPENTHREAD_LOG_LEVEL_INFO=y
-
-# Adjust log strdup settings
-CONFIG_LOG_STRDUP_BUF_COUNT=32
-CONFIG_LOG_STRDUP_MAX_STRING=128
diff --git a/overlay-mtd.conf b/overlay-mtd.conf
deleted file mode 100644
index 8d78608..0000000
--- a/overlay-mtd.conf
+++ /dev/null
@@ -1,15 +0,0 @@
-#
-# Copyright (c) 2020 Nordic Semiconductor
-#
-# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
-#
-
-# Enable MTD Sleepy End Device
-CONFIG_OPENTHREAD_MTD=y
-CONFIG_OPENTHREAD_MTD_SED=y
-CONFIG_OPENTHREAD_POLL_PERIOD=3000
-CONFIG_RAM_POWER_DOWN_LIBRARY=y
-CONFIG_PM_DEVICE=y
-
-# This variant requires increased system workqueue stack size
-CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=1536
diff --git a/overlay-rtt.conf b/overlay-rtt.conf
deleted file mode 100644
index c4b8f35..0000000
--- a/overlay-rtt.conf
+++ /dev/null
@@ -1,13 +0,0 @@
-#
-# Copyright (c) 2022 Nordic Semiconductor
-#
-# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
-#
-
-# Enable RTT logging backend
-CONFIG_USE_SEGGER_RTT=y
-CONFIG_LOG_BACKEND_RTT=y
-
-# Disable UART logging backend
-CONFIG_LOG_BACKEND_UART=n
-CONFIG_SHELL_LOG_BACKEND=n
diff --git a/overlay-usb.conf b/overlay-usb.conf
deleted file mode 100644
index db09a1e..0000000
--- a/overlay-usb.conf
+++ /dev/null
@@ -1,17 +0,0 @@
-#
-# Copyright (c) 2021 Nordic Semiconductor
-#
-# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
-#
-
-CONFIG_SHELL_BACKEND_SERIAL_INIT_PRIORITY=51
-
-CONFIG_UART_LINE_CTRL=y
-CONFIG_SHELL_BACKEND_SERIAL_CHECK_DTR=y
-CONFIG_USB_CDC_ACM_LOG_LEVEL_OFF=y
-
-CONFIG_USB_DEVICE_STACK=y
-CONFIG_USB_DEVICE_MANUFACTURER="Nordic Semiconductor ASA"
-CONFIG_USB_DEVICE_PRODUCT="Thread CLI"
-CONFIG_USB_DEVICE_VID=0x1915
-CONFIG_USB_DEVICE_PID=0x0000
diff --git a/prj.conf b/prj.conf
index 8c06b25..974d6d9 100644
--- a/prj.conf
+++ b/prj.conf
@@ -1,98 +1,67 @@
-# Nordic Dev Kit Library
-CONFIG_DK_LIBRARY=y
+# Copyright (c) 2022-2023 Golioth, Inc.
+# SPDX-License-Identifier: Apache-2.0
 
-CONFIG_LOG=y
-CONFIG_GOLIOTH_THREAD_LOG_LEVEL_DBG=y
+# Enable Golioth Firmware SDK
+CONFIG_GOLIOTH_FIRMWARE_SDK=y
 
-CONFIG_NETWORKING=y
-CONFIG_NET_L2_OPENTHREAD=y
-
-CONFIG_SHELL=y
-CONFIG_NET_SHELL=y
-CONFIG_OPENTHREAD_SHELL=y
-CONFIG_SHELL_ARGC_MAX=26
-CONFIG_SHELL_CMD_BUFF_SIZE=416
-
-# IMPORTANT: Change the Thread network credentials to match your Thread network setup
-CONFIG_OPENTHREAD_NORDIC_LIBRARY_MASTER=y
-CONFIG_OPENTHREAD_NETWORK_NAME="OpenThreadDemo"
-CONFIG_OPENTHREAD_NETWORKKEY="00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff"
+# Golioth services used in this app
+CONFIG_GOLIOTH_FW_UPDATE=y
+CONFIG_GOLIOTH_LIGHTDB_STATE=y
+CONFIG_LOG_BACKEND_GOLIOTH=y
+CONFIG_GOLIOTH_RPC=y
+CONFIG_GOLIOTH_SETTINGS=y
+CONFIG_GOLIOTH_STREAM=y
 
-CONFIG_MBEDTLS_SHA1_C=n
-CONFIG_FPU=y
+# Enable common sample library
+CONFIG_GOLIOTH_SAMPLE_COMMON=y
 
-# TLS configuration
-CONFIG_MBEDTLS=y
-CONFIG_MBEDTLS_BUILTIN=n
+# Configure Golioth SDK dependencies
+CONFIG_EVENTFD_MAX=14
+CONFIG_LOG_PROCESS_THREAD_STACK_SIZE=2048
 CONFIG_MBEDTLS_ENABLE_HEAP=y
 CONFIG_MBEDTLS_HEAP_SIZE=10240
-# PSK needs to be manually enabled to prevent ENOTSUP (-134)
-CONFIG_MBEDTLS_KEY_EXCHANGE_SOME_PSK_ENABLED=y
-CONFIG_MBEDTLS_KEY_EXCHANGE_PSK_ENABLED=y
-CONFIG_NET_SOCKETS_SOCKOPT_TLS=y
-
-# Golioth-specific configuration
-CONFIG_GOLIOTH=y
-CONFIG_GOLIOTH_SYSTEM_CLIENT=y
-
-
-CONFIG_LOG_BACKEND_GOLIOTH=y
-CONFIG_LOG_PROCESS_THREAD_STACK_SIZE=2048
-
-# NAT64-prefixed coap.golioth.io IPv4
-# TODO: use DNS to resolve an IPv6-specific hostname to IPv6
-CONFIG_GOLIOTH_SYSTEM_SERVER_HOST="64:ff9b::2287:5a70"
-
-CONFIG_BOOTLOADER_MCUBOOT=n
-
-#Increase TLS message size
-
-CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=1400
-
-#Sensor config
-
-CONFIG_I2C=y
-CONFIG_SENSOR=y
-CONFIG_SI7055=y
-
-# Debug
-
-CONFIG_THREAD_NAME=y
-CONFIG_SCHED_CPU_MASK=y
-CONFIG_THREAD_ANALYZER=y
-
-CONFIG_THREAD_NAME=y
-CONFIG_SEGGER_SYSTEMVIEW=y
-CONFIG_USE_SEGGER_RTT=y
-CONFIG_TRACING=y
-CONFIG_TRACING_BACKEND_RAM=y
-
-# Shell setup
-
-CONFIG_GPIO=y
-CONFIG_I2C=y
- 
-CONFIG_SHELL=y
-CONFIG_I2C_SHELL=y
+CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=2048
+CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=2048
+CONFIG_NETWORKING=y
+CONFIG_NET_IPV4=y
+CONFIG_POSIX_MAX_FDS=23
 
-CONFIG_ADC=y
-CONFIG_ADC_SHELL=y
+# Application
+CONFIG_MAIN_STACK_SIZE=2048
+CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048
+CONFIG_NET_LOG=y
+CONFIG_NET_SHELL=y
+CONFIG_REBOOT=y
 
-# Settings shell
+# Flash memory (etc.) for firmware upgrade
+CONFIG_FLASH=y
+CONFIG_FLASH_MAP=y
+CONFIG_NVS=y
+CONFIG_STREAM_FLASH=y
+CONFIG_IMG_MANAGER=y
+CONFIG_IMG_ERASE_PROGRESSIVELY=y
+CONFIG_REBOOT=y
 
+# The rest of the runtime credentials config
 CONFIG_SETTINGS=y
 CONFIG_SETTINGS_RUNTIME=y
+CONFIG_GOLIOTH_SAMPLE_PSK_SETTINGS=y
 CONFIG_GOLIOTH_SAMPLE_SETTINGS_AUTOLOAD=y
-CONFIG_GOLIOTH_SAMPLES_COMMON=y
-CONFIG_GOLIOTH_SYSTEM_SETTINGS=y
 CONFIG_GOLIOTH_SAMPLE_SETTINGS_SHELL=y
 
-CONFIG_FLASH=y
-CONFIG_FLASH_MAP=y
-CONFIG_NVS=y
+# Misc.
+CONFIG_JSON_LIBRARY=y
+# Longer response length needed for network info
+CONFIG_GOLIOTH_RPC_MAX_RESPONSE_LEN=512
 
-CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=8192
+# Generate MCUboot compatible images
+CONFIG_BOOTLOADER_MCUBOOT=y
 
-CONFIG_GOLIOTH_SYSTEM_CLIENT_LOG_LEVEL_DBG=n
+# Firmware version used in DFU process
+CONFIG_MCUBOOT_IMGTOOL_SIGN_VERSION="1.0.0"
 
-CONFIG_OPENTHREAD_DEBUG=n
\ No newline at end of file
+# IMPORTANT: Change the Thread network credentials to match your Thread network setup
+CONFIG_OPENTHREAD_NORDIC_LIBRARY_MASTER=y
+CONFIG_OPENTHREAD_CHANNEL=26
+CONFIG_OPENTHREAD_NETWORKKEY="00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff"
+CONFIG_OPENTHREAD_NETWORK_NAME="golioth-thread"
diff --git a/sample.yaml b/sample.yaml
index aef1e9c..f1692b5 100644
--- a/sample.yaml
+++ b/sample.yaml
@@ -1,16 +1,16 @@
+# Copyright (c) 2022-2024 Golioth, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
 sample:
   description: Golioth Thread sample with OpenThread and IPv6
   name: openthread
 
 common:
-  platform_allow: nrf5340dk_nrf5340_cpuapp nrf5340dk_nrf5340_cpuapp_ns nrf52840dk_nrf52840 nrf52833dk_nrf52833 nrf21540dk_nrf52840
+  platform_allow: nrf52840dk_nrf52840 adafruit_feather_nrf52840
   tags: golioth net socket thread
   integration_platforms:
-    - nrf5340dk_nrf5340_cpuapp
-    - nrf5340dk_nrf5340_cpuapp_ns
     - nrf52840dk_nrf52840
-    - nrf52833dk_nrf52833
-    - nrf21540dk_nrf52840
+    - adafruit_feather_nrf52840
 
 tests:
   sample.golioth.openthread
diff --git a/src/app_rpc.c b/src/app_rpc.c
new file mode 100644
index 0000000..c949cb0
--- /dev/null
+++ b/src/app_rpc.c
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2022-2023 Golioth, Inc.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <zephyr/logging/log.h>
+#include <zephyr/logging/log_ctrl.h>
+LOG_MODULE_REGISTER(app_rpc, LOG_LEVEL_DBG);
+
+#include <golioth/client.h>
+#include <golioth/rpc.h>
+#include <zephyr/logging/log_ctrl.h>
+#include <zephyr/sys/reboot.h>
+
+#include "app_rpc.h"
+
+static void reboot_work_handler(struct k_work *work)
+{
+	for (int8_t i = 5; i >= 0; i--) {
+		if (i) {
+			LOG_INF("Rebooting in %d seconds...", i);
+		}
+		k_sleep(K_SECONDS(1));
+	}
+
+	/* Sync logs before reboot */
+	LOG_PANIC();
+
+	sys_reboot(SYS_REBOOT_COLD);
+}
+K_WORK_DEFINE(reboot_work, reboot_work_handler);
+
+static enum golioth_rpc_status on_set_log_level(zcbor_state_t *request_params_array,
+						zcbor_state_t *response_detail_map,
+						void *callback_arg)
+{
+	double param_0;
+	uint8_t log_level;
+	bool ok;
+
+	LOG_WRN("on_set_log_level");
+
+	ok = zcbor_float_decode(request_params_array, &param_0);
+	if (!ok) {
+		LOG_ERR("Failed to decode array item");
+		return GOLIOTH_RPC_INVALID_ARGUMENT;
+	}
+
+	log_level = (uint8_t)param_0;
+
+	if ((log_level < 0) || (log_level > LOG_LEVEL_DBG)) {
+
+		LOG_ERR("Requested log level is out of bounds: %d", log_level);
+		return GOLIOTH_RPC_INVALID_ARGUMENT;
+	}
+
+	int source_id = 0;
+	char *source_name;
+
+	while (1) {
+		source_name = (char *)log_source_name_get(0, source_id);
+		if (source_name == NULL) {
+			break;
+		}
+
+		log_filter_set(NULL, 0, source_id, log_level);
+		++source_id;
+	}
+
+	LOG_WRN("Log levels for %d modules set to: %d", source_id, log_level);
+
+	ok = zcbor_tstr_put_lit(response_detail_map, "log_modules") &&
+	     zcbor_float64_put(response_detail_map, (double)source_id);
+
+	return GOLIOTH_RPC_OK;
+}
+
+static enum golioth_rpc_status on_reboot(zcbor_state_t *request_params_array,
+					 zcbor_state_t *response_detail_map,
+					 void *callback_arg)
+{
+	/* Use work queue so this RPC can return confirmation to Golioth */
+	k_work_submit(&reboot_work);
+
+	return GOLIOTH_RPC_OK;
+}
+
+static void rpc_log_if_register_failure(int err)
+{
+	if (err) {
+		LOG_ERR("Failed to register RPC: %d", err);
+	}
+}
+
+void app_rpc_register(struct golioth_client *client)
+{
+	struct golioth_rpc *rpc = golioth_rpc_init(client);
+
+	int err;
+
+	err = golioth_rpc_register(rpc, "reboot", on_reboot, NULL);
+	rpc_log_if_register_failure(err);
+
+	err = golioth_rpc_register(rpc, "set_log_level", on_set_log_level, NULL);
+	rpc_log_if_register_failure(err);
+}
diff --git a/src/app_rpc.h b/src/app_rpc.h
new file mode 100644
index 0000000..4793235
--- /dev/null
+++ b/src/app_rpc.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2022-2023 Golioth, Inc.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * Handle remote procedure calls received from Golioth, returning a status code
+ * indicating the success or failure of the call.
+ *
+ * This demonstration implements the following RPCs:
+ * - `get_network_info`: Query and return network information.
+ * - `reboot`: reboot the device (no arguments)
+ * - `set_log_level`: adjust the logging level for all registered modules (valid
+ *   argument values: 0..4)
+ *
+ * https://docs.golioth.io/firmware/zephyr-device-sdk/remote-procedure-call
+ */
+
+#ifndef __APP_RPC_H__
+#define __APP_RPC_H__
+
+#include <golioth/client.h>
+
+void app_rpc_register(struct golioth_client *client);
+
+#endif /* __APP_RPC_H__ */
diff --git a/src/app_sensors.c b/src/app_sensors.c
new file mode 100644
index 0000000..a68f86c
--- /dev/null
+++ b/src/app_sensors.c
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2022-2023 Golioth, Inc.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <zephyr/logging/log.h>
+LOG_MODULE_REGISTER(app_sensors, LOG_LEVEL_DBG);
+
+#include <golioth/client.h>
+#include <golioth/stream.h>
+#include <zephyr/drivers/gpio.h>
+#include <zephyr/kernel.h>
+
+#include "app_sensors.h"
+
+static struct golioth_client *client;
+/* Add Sensor structs here */
+
+/* Formatting string for sending sensor JSON to Golioth */
+#define JSON_FMT "{\"counter\":%d}"
+
+/* Callback for LightDB Stream */
+
+static void async_error_handler(struct golioth_client *client,
+				const struct golioth_response *response,
+				const char *path,
+				void *arg)
+{
+	if (response->status != GOLIOTH_OK) {
+		LOG_ERR("Async task failed: %d", response->status);
+		return;
+	}
+}
+
+/* This will be called by the main() loop */
+/* Do all of your work here! */
+void app_sensors_read_and_stream(void)
+{
+	int err;
+	char json_buf[256];
+
+	static uint8_t counter;
+
+	/* Send sensor data to Golioth */
+	/* For this demo we just fake it */
+	snprintk(json_buf, sizeof(json_buf), JSON_FMT, counter);
+	LOG_DBG("%s", json_buf);
+
+	err = golioth_stream_set_async(client,
+				       "sensor",
+				       GOLIOTH_CONTENT_TYPE_JSON,
+				       json_buf,
+				       strlen(json_buf),
+				       async_error_handler,
+				       NULL);
+	if (err) {
+		LOG_ERR("Failed to send sensor data to Golioth: %d", err);
+	}
+
+	++counter;
+}
+
+void app_sensors_set_client(struct golioth_client *sensors_client)
+{
+	client = sensors_client;
+}
diff --git a/src/app_sensors.h b/src/app_sensors.h
new file mode 100644
index 0000000..03e906b
--- /dev/null
+++ b/src/app_sensors.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2022-2023 Golioth, Inc.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#ifndef __APP_SENSORS_H__
+#define __APP_SENSORS_H__
+
+/** The `app_sensors.c` file performs the important work of this application
+ * which is to read sensor values and report them to the Golioth LightDB Stream
+ * as time-series data.
+ *
+ * For this demonstration, a `counter` value is periodically logged and pushed
+ * to the Golioth time-series database. This simulated sensor reading occurs
+ * when the loop in `main.c` calls `app_sensors_read_and_stream()`. The
+ * frequency of this loop is determined by values received from the Golioth
+ * Settings Service (see app_settings.h).
+ *
+ * https://docs.golioth.io/firmware/zephyr-device-sdk/light-db-stream/
+ */
+
+#include <golioth/client.h>
+
+void app_sensors_set_client(struct golioth_client *sensors_client);
+void app_sensors_read_and_stream(void);
+
+#endif /* __APP_SENSORS_H__ */
diff --git a/src/app_settings.c b/src/app_settings.c
new file mode 100644
index 0000000..0dbba5b
--- /dev/null
+++ b/src/app_settings.c
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2022-2023 Golioth, Inc.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <zephyr/logging/log.h>
+LOG_MODULE_REGISTER(app_settings, LOG_LEVEL_DBG);
+
+#include <golioth/client.h>
+#include <golioth/settings.h>
+#include "main.h"
+#include "app_settings.h"
+
+static int32_t _loop_delay_s = 60;
+#define LOOP_DELAY_S_MAX 43200
+#define LOOP_DELAY_S_MIN 0
+
+int32_t get_loop_delay_s(void)
+{
+	return _loop_delay_s;
+}
+
+static enum golioth_settings_status on_loop_delay_setting(int32_t new_value, void *arg)
+{
+	_loop_delay_s = new_value;
+	LOG_INF("Set loop delay to %i seconds", new_value);
+	wake_system_thread();
+	return GOLIOTH_SETTINGS_SUCCESS;
+}
+
+int app_settings_register(struct golioth_client *client)
+{
+	struct golioth_settings *settings = golioth_settings_init(client);
+
+	int err = golioth_settings_register_int_with_range(settings,
+							   "LOOP_DELAY_S",
+							   LOOP_DELAY_S_MIN,
+							   LOOP_DELAY_S_MAX,
+							   on_loop_delay_setting,
+							   NULL);
+
+	if (err) {
+		LOG_ERR("Failed to register settings callback: %d", err);
+	}
+
+	return err;
+}
diff --git a/src/app_settings.h b/src/app_settings.h
new file mode 100644
index 0000000..381d144
--- /dev/null
+++ b/src/app_settings.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2022-2023 Golioth, Inc.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * Process changes received from the Golioth Settings Service and return a code
+ * to Golioth to indicate the success or failure of the update.
+ *
+ * In this demonstration, the device looks for the `LOOP_DELAY_S` key from the
+ * Settings Service and uses this value to determine the delay between sensor
+ * reads (the period of sleep in the loop of `main.c`.
+ *
+ * https://docs.golioth.io/firmware/zephyr-device-sdk/device-settings-service
+ */
+
+#ifndef __APP_SETTINGS_H__
+#define __APP_SETTINGS_H__
+
+#include <stdint.h>
+#include <golioth/client.h>
+
+int32_t get_loop_delay_s(void);
+int app_settings_register(struct golioth_client *client);
+
+#endif /* __APP_SETTINGS_H__ */
diff --git a/src/app_state.c b/src/app_state.c
new file mode 100644
index 0000000..961aceb
--- /dev/null
+++ b/src/app_state.c
@@ -0,0 +1,193 @@
+/*
+ * Copyright (c) 2022-2023 Golioth, Inc.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <zephyr/logging/log.h>
+LOG_MODULE_REGISTER(app_state, LOG_LEVEL_DBG);
+
+#include <golioth/client.h>
+#include <golioth/lightdb_state.h>
+#include <zephyr/data/json.h>
+#include <zephyr/kernel.h>
+#include "json_helper.h"
+
+#include "app_state.h"
+#include "app_sensors.h"
+
+#define DEVICE_STATE_FMT "{\"example_int0\":%d,\"example_int1\":%d}"
+
+uint32_t _example_int0;
+uint32_t _example_int1 = 1;
+
+static struct golioth_client *client;
+
+static void async_handler(struct golioth_client *client,
+				       const struct golioth_response *response,
+				       const char *path,
+				       void *arg)
+{
+	if (response->status != GOLIOTH_OK) {
+		LOG_WRN("Failed to set state: %d", response->status);
+		return;
+	}
+
+	LOG_DBG("State successfully set");
+}
+
+int app_state_reset_desired(void)
+{
+	LOG_INF("Resetting \"%s\" LightDB State endpoint to defaults.", APP_STATE_DESIRED_ENDP);
+
+	char sbuf[sizeof(DEVICE_STATE_FMT) + 4]; /* space for two "-1" values */
+
+	snprintk(sbuf, sizeof(sbuf), DEVICE_STATE_FMT, -1, -1);
+
+	int err;
+	err = golioth_lightdb_set_async(client,
+					APP_STATE_DESIRED_ENDP,
+					GOLIOTH_CONTENT_TYPE_JSON,
+					sbuf,
+					strlen(sbuf),
+					async_handler,
+					NULL);
+	if (err) {
+		LOG_ERR("Unable to write to LightDB State: %d", err);
+	}
+	return err;
+}
+
+int app_state_update_actual(void)
+{
+
+	char sbuf[sizeof(DEVICE_STATE_FMT) + 10]; /* space for uint16 values */
+
+	snprintk(sbuf, sizeof(sbuf), DEVICE_STATE_FMT, _example_int0, _example_int1);
+
+	int err;
+
+	err = golioth_lightdb_set_async(client,
+					APP_STATE_ACTUAL_ENDP,
+					GOLIOTH_CONTENT_TYPE_JSON,
+					sbuf,
+					strlen(sbuf),
+					async_handler,
+					NULL);
+
+	if (err) {
+		LOG_ERR("Unable to write to LightDB State: %d", err);
+	}
+	return err;
+}
+
+static void app_state_desired_handler(struct golioth_client *client,
+				      const struct golioth_response *response,
+				      const char *path,
+				      const uint8_t *payload,
+				      size_t payload_size,
+				      void *arg)
+{
+	int err = 0;
+	int ret;
+
+	if (response->status != GOLIOTH_OK) {
+		LOG_ERR("Failed to receive '%s' endpoint: %d",
+			APP_STATE_DESIRED_ENDP,
+			response->status);
+		return;
+	}
+
+	LOG_HEXDUMP_DBG(payload, payload_size, APP_STATE_DESIRED_ENDP);
+
+	struct app_state parsed_state;
+
+	ret = json_obj_parse((char *)payload, payload_size, app_state_descr,
+			     ARRAY_SIZE(app_state_descr), &parsed_state);
+
+	if (ret < 0) {
+		LOG_ERR("Error parsing desired values: %d", ret);
+		app_state_reset_desired();
+		return;
+	}
+
+	uint8_t desired_processed_count = 0;
+	uint8_t state_change_count = 0;
+
+	if (ret & 1 << 0) {
+		/* Process example_int0 */
+		if ((parsed_state.example_int0 >= 0) && (parsed_state.example_int0 < 65536)) {
+			LOG_DBG("Validated desired example_int0 value: %d",
+				parsed_state.example_int0);
+			if (_example_int0 != parsed_state.example_int0) {
+				_example_int0 = parsed_state.example_int0;
+				++state_change_count;
+			}
+			++desired_processed_count;
+		} else if (parsed_state.example_int0 == -1) {
+			LOG_DBG("No change requested for example_int0");
+		} else {
+			LOG_ERR("Invalid desired example_int0 value: %d",
+				parsed_state.example_int0);
+			++desired_processed_count;
+		}
+	}
+	if (ret & 1 << 1) {
+		/* Process example_int1 */
+		if ((parsed_state.example_int1 >= 0) && (parsed_state.example_int1 < 65536)) {
+			LOG_DBG("Validated desired example_int1 value: %d",
+				parsed_state.example_int1);
+			if (_example_int1 != parsed_state.example_int1) {
+				_example_int1 = parsed_state.example_int1;
+				++state_change_count;
+			}
+			++desired_processed_count;
+		} else if (parsed_state.example_int1 == -1) {
+			LOG_DBG("No change requested for example_int1");
+		} else {
+			LOG_ERR("Invalid desired example_int1 value: %d",
+				parsed_state.example_int1);
+			++desired_processed_count;
+		}
+	}
+
+	if (state_change_count) {
+		/* The state was changed, so update the state on the Golioth servers */
+		err = app_state_update_actual();
+	}
+	if (desired_processed_count) {
+		/* We processed some desired changes to return these to -1 on the server
+		 * to indicate the desired values were received.
+		 */
+		err = app_state_reset_desired();
+	}
+
+	if (err) {
+		LOG_ERR("Failed to update cloud state: %d", err);
+	}
+}
+
+int app_state_observe(struct golioth_client *state_client)
+{
+	int err;
+
+	client = state_client;
+
+	err = golioth_lightdb_observe_async(client,
+					    APP_STATE_DESIRED_ENDP,
+					    GOLIOTH_CONTENT_TYPE_JSON,
+					    app_state_desired_handler,
+					    NULL);
+	if (err) {
+		LOG_WRN("failed to observe lightdb path: %d", err);
+		return err;
+	}
+
+	/* This will only run once. It updates the actual state of the device
+	 * with the Golioth servers. Future updates will be sent whenever
+	 * changes occur.
+	 */
+	err = app_state_update_actual();
+
+	return err;
+}
diff --git a/src/app_state.h b/src/app_state.h
new file mode 100644
index 0000000..a00222a
--- /dev/null
+++ b/src/app_state.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2022-2023 Golioth, Inc.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/** Observe and write to example endpoints for stateful data on the Golioth
+ * LightDB State Service.
+ *
+ * This demonstration exhibits (the concept of Digital
+ * Twin)[https://blog.golioth.io/better-iot-design-patterns-desired-state-vs-actual-state/].
+ * It implements a _desired_ state which the cloud can set to request the device
+ * change its state, and an _actual_ state where the device reports its state.
+ *
+ * After receiving and processing a desired state, the device will reset the
+ * desired state (`APP_STATE_DESIRED_ENDP`) to `-1` indicating the data has been
+ * processed, and update the actual state (`APP_STATE_ACTUAL_ENDP`) to report
+ * the new state of the device.
+ *
+ * The device should write to the _actual state_ endpoint, the cloud should not.
+ * By convention the cloud should consider the _actual state_ values read-only.
+ *
+ * https://docs.golioth.io/firmware/zephyr-device-sdk/light-db/
+ */
+
+#ifndef __APP_STATE_H__
+#define __APP_STATE_H__
+
+#include <golioth/client.h>
+
+#define APP_STATE_DESIRED_ENDP "desired"
+#define APP_STATE_ACTUAL_ENDP  "state"
+
+int app_state_observe(struct golioth_client *state_client);
+int app_state_update_actual(void);
+
+#endif /* __APP_STATE_H__ */
diff --git a/src/battery_monitor/battery.c b/src/battery_monitor/battery.c
new file mode 100644
index 0000000..5e95a5f
--- /dev/null
+++ b/src/battery_monitor/battery.c
@@ -0,0 +1,377 @@
+/*
+ * Copyright (c) 2018-2019 Peter Bigot Consulting, LLC
+ * Copyright (c) 2019-2020 Nordic Semiconductor ASA
+ * Copyright (c) 2023 Golioth, Inc.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <zephyr/kernel.h>
+#include <zephyr/init.h>
+#include <zephyr/drivers/gpio.h>
+#include <zephyr/drivers/adc.h>
+#include <zephyr/logging/log.h>
+#include <golioth/client.h>
+#include <golioth/stream.h>
+
+#include "battery_monitor/battery.h"
+#include "../app_sensors.h"
+
+LOG_MODULE_REGISTER(battery, LOG_LEVEL_DBG);
+
+#define VBATT	    DT_PATH(vbatt)
+#define ZEPHYR_USER DT_PATH(zephyr_user)
+
+/* Formatting string for sending battery JSON to Golioth */
+#define JSON_FMT "{\"batt_v\":%d.%03d,\"batt_lvl\":%d.%02d}"
+
+#define LABEL_BATTERY "Battery"
+
+#ifdef CONFIG_BOARD_THINGY52_NRF52832
+/* This board uses a divider that reduces max voltage to
+ * reference voltage (600 mV).
+ */
+#define BATTERY_ADC_GAIN ADC_GAIN_1
+#else
+/* Other boards may use dividers that only reduce battery voltage to
+ * the maximum supported by the hardware (3.6 V)
+ */
+#define BATTERY_ADC_GAIN ADC_GAIN_1_6
+#endif
+
+char stream_endpoint[] = "battery";
+
+char _batt_v_str[8] = "0.0 V";
+char _batt_lvl_str[5] = "none";
+
+/* Battery values specific to the Aludel-mini */
+static const struct battery_level_point batt_levels[] = {
+	/* "Curve" here eyeballed from captured data for the [Adafruit
+	 * 3.7v 2000 mAh](https://www.adafruit.com/product/2011) LIPO
+	 * under full load that started with a charge of 3.96 V and
+	 * dropped about linearly to 3.58 V over 15 hours.  It then
+	 * dropped rapidly to 3.10 V over one hour, at which point it
+	 * stopped transmitting.
+	 *
+	 * Based on eyeball comparisons we'll say that 15/16 of life
+	 * goes between 3.95 and 3.55 V, and 1/16 goes between 3.55 V
+	 * and 3.1 V.
+	 */
+
+	{10000, 3950},
+	{625, 3550},
+	{0, 3100},
+};
+
+struct io_channel_config {
+	uint8_t channel;
+};
+
+struct divider_config {
+	struct io_channel_config io_channel;
+	struct gpio_dt_spec power_gpios;
+	/* output_ohm is used as a flag value: if it is nonzero then
+	 * the battery is measured through a voltage divider;
+	 * otherwise it is assumed to be directly connected to Vdd.
+	 */
+	uint32_t output_ohm;
+	uint32_t full_ohm;
+};
+
+static const struct divider_config divider_config = {
+#if DT_NODE_HAS_STATUS(VBATT, okay)
+	/* clang-format off */
+	.io_channel = {
+		DT_IO_CHANNELS_INPUT(VBATT),
+	}, /* clang-format on */
+	.power_gpios = GPIO_DT_SPEC_GET_OR(VBATT, power_gpios, {}),
+	.output_ohm = DT_PROP(VBATT, output_ohms),
+	.full_ohm = DT_PROP(VBATT, full_ohms),
+#else  /* /vbatt exists */
+	/* clang-format off */
+	.io_channel = {
+		DT_IO_CHANNELS_INPUT(ZEPHYR_USER),
+	}, /* clang-format on */
+#endif /* /vbatt exists */
+};
+
+struct divider_data {
+	const struct device *adc;
+	struct adc_channel_cfg adc_cfg;
+	struct adc_sequence adc_seq;
+	int16_t raw;
+};
+static struct divider_data divider_data = {
+#if DT_NODE_HAS_STATUS(VBATT, okay)
+	.adc = DEVICE_DT_GET(DT_IO_CHANNELS_CTLR(VBATT)),
+#else
+	.adc = DEVICE_DT_GET(DT_IO_CHANNELS_CTLR(ZEPHYR_USER)),
+#endif
+};
+
+static int divider_setup(void)
+{
+	const struct divider_config *cfg = &divider_config;
+	const struct io_channel_config *iocp = &cfg->io_channel;
+	const struct gpio_dt_spec *gcp = &cfg->power_gpios;
+	struct divider_data *ddp = &divider_data;
+	struct adc_sequence *asp = &ddp->adc_seq;
+	struct adc_channel_cfg *accp = &ddp->adc_cfg;
+	int rc;
+
+	if (!device_is_ready(ddp->adc)) {
+		LOG_ERR("ADC device is not ready %s", ddp->adc->name);
+		return -ENOENT;
+	}
+
+	if (gcp->port) {
+		if (!device_is_ready(gcp->port)) {
+			LOG_ERR("%s: device not ready", gcp->port->name);
+			return -ENOENT;
+		}
+		rc = gpio_pin_configure_dt(gcp, GPIO_OUTPUT_INACTIVE);
+		if (rc != 0) {
+			LOG_ERR("Failed to control feed %s.%u: %d", gcp->port->name, gcp->pin, rc);
+			return rc;
+		}
+	}
+
+	*asp = (struct adc_sequence){
+		.channels = BIT(0),
+		.buffer = &ddp->raw,
+		.buffer_size = sizeof(ddp->raw),
+		.oversampling = 4,
+		.calibrate = true,
+	};
+
+#ifdef CONFIG_ADC_NRFX_SAADC
+	*accp = (struct adc_channel_cfg){
+		.gain = BATTERY_ADC_GAIN,
+		.reference = ADC_REF_INTERNAL,
+		.acquisition_time = ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 40),
+	};
+
+	if (cfg->output_ohm != 0) {
+		accp->input_positive = SAADC_CH_PSELP_PSELP_AnalogInput0 + iocp->channel;
+	} else {
+		accp->input_positive = SAADC_CH_PSELP_PSELP_VDD;
+	}
+
+	asp->resolution = 14;
+#else /* CONFIG_ADC_var */
+#error Unsupported ADC
+#endif /* CONFIG_ADC_var */
+
+	rc = adc_channel_setup(ddp->adc, accp);
+	if (rc) {
+		LOG_ERR("Failed to setup ADC for AIN%u: %d", iocp->channel, rc);
+	} else {
+		LOG_DBG("ADC setup for AIN%u complete", iocp->channel);
+	}
+
+	return rc;
+}
+
+static bool battery_ok;
+
+static int battery_setup(void)
+{
+	LOG_INF("Initializing battery measurement");
+
+	int rc = divider_setup();
+
+	battery_ok = (rc == 0);
+	if (rc) {
+		LOG_ERR("Battery measurement setup failed: %d", rc);
+	}
+
+	return rc;
+}
+
+SYS_INIT(battery_setup, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY);
+
+int battery_measure_enable(bool enable)
+{
+	int rc = -ENOENT;
+
+	if (battery_ok) {
+		const struct gpio_dt_spec *gcp = &divider_config.power_gpios;
+
+		rc = 0;
+		if (gcp->port) {
+			rc = gpio_pin_set_dt(gcp, enable);
+		}
+	}
+	return rc;
+}
+
+int battery_sample(void)
+{
+	int rc = -ENOENT;
+
+	if (battery_ok) {
+		struct divider_data *ddp = &divider_data;
+		const struct divider_config *dcp = &divider_config;
+		struct adc_sequence *sp = &ddp->adc_seq;
+
+		rc = adc_read(ddp->adc, sp);
+		sp->calibrate = false;
+		if (rc == 0) {
+			int32_t val = ddp->raw;
+
+			adc_raw_to_millivolts(adc_ref_internal(ddp->adc), ddp->adc_cfg.gain,
+					      sp->resolution, &val);
+
+			if (dcp->output_ohm != 0) {
+				rc = val * (uint64_t)dcp->full_ohm / dcp->output_ohm;
+				LOG_DBG("raw %u ~ %u mV => %d mV", ddp->raw, val, rc);
+			} else {
+				rc = val;
+				LOG_DBG("raw %u ~ %u mV", ddp->raw, val);
+			}
+		}
+	}
+
+	return rc;
+}
+
+unsigned int battery_level_pptt(unsigned int batt_mV, const struct battery_level_point *curve)
+{
+	const struct battery_level_point *pb = curve;
+
+	if (batt_mV >= pb->lvl_mV) {
+		/* Measured voltage above highest point, cap at maximum. */
+		return pb->lvl_pptt;
+	}
+	/* Go down to the last point at or below the measured voltage. */
+	while ((pb->lvl_pptt > 0) && (batt_mV < pb->lvl_mV)) {
+		++pb;
+	}
+	if (batt_mV < pb->lvl_mV) {
+		/* Below lowest point, cap at minimum */
+		return pb->lvl_pptt;
+	}
+
+	/* Linear interpolation between below and above points. */
+	const struct battery_level_point *pa = pb - 1;
+
+	return pb->lvl_pptt +
+	       ((pa->lvl_pptt - pb->lvl_pptt) * (batt_mV - pb->lvl_mV) / (pa->lvl_mV - pb->lvl_mV));
+}
+
+int read_battery_data(struct battery_data *batt_data)
+{
+
+	/* Turn on the voltage divider circuit */
+	int err = battery_measure_enable(true);
+
+	if (err) {
+		LOG_ERR("Failed to enable battery measurement power: %d", err);
+		return err;
+	}
+
+	/* Read the battery voltage */
+	int batt_mv = battery_sample();
+
+	if (batt_mv < 0) {
+		LOG_ERR("Failed to read battery voltage: %d", batt_mv);
+		return batt_mv;
+	}
+
+	/* Turn off the voltage divider circuit */
+	err = battery_measure_enable(false);
+	if (err) {
+		LOG_ERR("Failed to disable battery measurement power: %d", err);
+		return err;
+	}
+
+	batt_data->battery_voltage_mv = batt_mv;
+	batt_data->battery_level_pptt = battery_level_pptt(batt_mv, batt_levels);
+
+	return 0;
+}
+
+char *get_batt_v_str(void)
+{
+	return _batt_v_str;
+}
+
+char *get_batt_lvl_str(void)
+{
+	return _batt_lvl_str;
+}
+
+void log_battery_data(void)
+{
+	LOG_INF("Battery measurement: voltage=%s, level=%s", get_batt_v_str(), get_batt_lvl_str());
+}
+
+static void async_error_handler(struct golioth_client *client,
+				const struct golioth_response *response,
+				const char *path,
+				void *arg)
+{
+	if (response->status != GOLIOTH_OK) {
+		LOG_ERR("Failed to stream battery data: %d", response->status);
+		return;
+	}
+}
+
+int stream_battery_data(struct golioth_client *client, struct battery_data *batt_data)
+{
+	int err;
+	/* {"batt_v":X.XXX,"batt_lvl":XXX.XX} */
+	char json_buf[35];
+
+	/* Send battery data to Golioth */
+	snprintk(json_buf, sizeof(json_buf), JSON_FMT, batt_data->battery_voltage_mv / 1000,
+		 batt_data->battery_voltage_mv % 1000, batt_data->battery_level_pptt / 100,
+		 batt_data->battery_level_pptt % 100);
+	LOG_DBG("%s", json_buf);
+
+	err = golioth_stream_set_async(client,
+				       stream_endpoint,
+				       GOLIOTH_CONTENT_TYPE_JSON,
+				       json_buf,
+				       strlen(json_buf),
+				       async_error_handler,
+				       NULL);
+	if (err) {
+		LOG_ERR("Failed to send battery data to Golioth: %d", err);
+	}
+
+	return 0;
+}
+
+int read_and_report_battery(struct golioth_client *client)
+{
+	int err;
+	struct battery_data batt_data;
+
+	err = read_battery_data(&batt_data);
+	if (err) {
+		LOG_ERR("Error reading battery data");
+		return err;
+	}
+
+	/* Format as global string for easy access */
+	snprintk(_batt_v_str, sizeof(_batt_v_str), "%d.%03d V", batt_data.battery_voltage_mv / 1000,
+		 batt_data.battery_voltage_mv % 1000);
+	snprintk(_batt_lvl_str, sizeof(_batt_lvl_str), "%d%%", batt_data.battery_level_pptt / 100);
+
+	log_battery_data();
+
+	if (golioth_client_is_connected(client)) {
+		err = stream_battery_data(client, &batt_data);
+		if (err) {
+			LOG_ERR("Error streaming battery info");
+			return err;
+		}
+	}
+
+	return 0;
+}
diff --git a/src/battery_monitor/include/battery_monitor/battery.h b/src/battery_monitor/include/battery_monitor/battery.h
new file mode 100644
index 0000000..35680cb
--- /dev/null
+++ b/src/battery_monitor/include/battery_monitor/battery.h
@@ -0,0 +1,123 @@
+/*
+ * Copyright (c) 2018-2019 Peter Bigot Consulting, LLC
+ * Copyright (c) 2023 Golioth, Inc.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#ifndef APPLICATION_BATTERY_H_
+#define APPLICATION_BATTERY_H_
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <golioth/client.h>
+
+/** Enable or disable measurement of the battery voltage.
+ *
+ * @param enable true to enable, false to disable
+ *
+ * @return zero on success, or a negative error code.
+ */
+int battery_measure_enable(bool enable);
+
+/** Measure the battery voltage.
+ *
+ * @return the battery voltage in millivolts, or a negative error
+ * code.
+ */
+int battery_sample(void);
+
+/** A point in a battery discharge curve sequence.
+ *
+ * A discharge curve is defined as a sequence of these points, where
+ * the first point has #lvl_pptt set to 10000 and the last point has
+ * #lvl_pptt set to zero.  Both #lvl_pptt and #lvl_mV should be
+ * monotonic decreasing within the sequence.
+ */
+struct battery_level_point {
+	/** Remaining life at #lvl_mV. */
+	uint16_t lvl_pptt;
+
+	/** Battery voltage at #lvl_pptt remaining life. */
+	uint16_t lvl_mV;
+};
+
+/** Calculate the estimated battery level based on a measured voltage.
+ *
+ * @param batt_mV a measured battery voltage level.
+ *
+ * @param curve the discharge curve for the type of battery installed
+ * on the system.
+ *
+ * @return the estimated remaining capacity in parts per ten
+ * thousand.
+ */
+unsigned int battery_level_pptt(unsigned int batt_mV, const struct battery_level_point *curve);
+
+/** A battery voltage and level measurement.
+ *
+ * Battery voltage is in mV.
+ * Battery level is in parts per ten thousand.
+ */
+struct battery_data {
+	int battery_voltage_mv;
+	unsigned int battery_level_pptt;
+};
+
+/**
+ * @brief Get pointer to a string representation of the last read battery
+ * voltage.
+ *
+ * This string is generated each time read_and_report_battery() is called.
+ *
+ * @return Pointer to character array
+ */
+char *get_batt_v_str(void);
+
+/**
+ * @brief Get pointer to a string representation of the last read percentage
+ * level. If a level has not yet been read, this value will be `none`.
+ *
+ * This string is generated each time read_and_report_battery() is called.
+ *
+ * @return Pointer to character array
+ */
+char *get_batt_lvl_str(void);
+
+/**
+ * @brief Read the battery voltage and estimated level.
+ *
+ * @param battery_data pointer to a struct to read the battery data into.
+ *
+ * @return Error number or zero if successful.
+ */
+int read_battery_data(struct battery_data *batt_data);
+
+/**
+ * @brief Log the battery voltage and estimated level.
+ *
+ * @param battery_data battery data to log.
+ *
+ */
+void log_battery_data(void);
+
+/**
+ * @brief Stream battery data to Golioth.
+ *
+ * @param client Golioth client to use for the Stream API call
+ * @param battery_data battery data to stream to Golioth.
+ *
+ * @return Error number or zero if successful
+ */
+int stream_battery_data(struct golioth_client *client, struct battery_data *batt_data);
+
+/**
+ * @brief Read, log, stream, and display a battery measurement.
+ *
+ * @param client Golioth client to use for the Stream API call
+ *
+ * @return Error number or zero if successful
+ */
+int read_and_report_battery(struct golioth_client *client);
+
+#endif /* APPLICATION_BATTERY_H_ */
diff --git a/src/json_helper.h b/src/json_helper.h
new file mode 100644
index 0000000..1de4379
--- /dev/null
+++ b/src/json_helper.h
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2023 Golioth, Inc.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#ifndef __JSON_HELPER_H_
+#define __JSON_HELPER_H_
+
+#include <zephyr/data/json.h>
+
+struct app_state {
+	int32_t example_int0;
+	int32_t example_int1;
+};
+
+static const struct json_obj_descr app_state_descr[] = {
+	JSON_OBJ_DESCR_PRIM(struct app_state, example_int0, JSON_TOK_NUMBER),
+	JSON_OBJ_DESCR_PRIM(struct app_state, example_int1, JSON_TOK_NUMBER)};
+
+#endif
diff --git a/src/main.c b/src/main.c
index 1107036..90d5986 100644
--- a/src/main.c
+++ b/src/main.c
@@ -1,205 +1,115 @@
-#include <dk_buttons_and_leds.h>
-#include <zephyr/logging/log.h>
-
-#include <zephyr/drivers/uart.h>
-#include <usb/usb_device.h>
-
-#include <net/golioth/system_client.h>
+/*
+ * Copyright (c) 2022-2023 Golioth, Inc.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
 
-#include <net/openthread.h>
+#include <zephyr/logging/log.h>
+LOG_MODULE_REGISTER(golioth_openthread_demo, LOG_LEVEL_DBG);
+
+#include "app_rpc.h"
+#include "app_settings.h"
+#include "app_state.h"
+#include "app_sensors.h"
+#include <golioth/client.h>
+#include <golioth/fw_update.h>
 #include <openthread/thread.h>
+#include <samples/common/net_connect.h>
+#include <samples/common/sample_credentials.h>
+#include <zephyr/drivers/gpio.h>
+#include "zephyr/kernel.h"
+#include <zephyr/net/coap.h>
+#include <zephyr/net/openthread.h>
+#include <zephyr/net/socket.h>
+#include "zephyr/sys/util_macro.h"
 
-#include <zephyr/drivers/sensor.h>
-#include <device.h>
-
-#include <init.h>
+/* Current firmware version; update in prj.conf or via build argument */
+static const char *_current_version = CONFIG_MCUBOOT_IMGTOOL_SIGN_VERSION;
 
+static struct golioth_client *client;
+K_SEM_DEFINE(connected, 0, 1);
 
+static k_tid_t _system_thread = 0;
 
-LOG_MODULE_REGISTER(red_demo_main, LOG_LEVEL_DBG);
+#if DT_NODE_EXISTS(DT_ALIAS(golioth_led))
+static const struct gpio_dt_spec golioth_led = GPIO_DT_SPEC_GET(DT_ALIAS(golioth_led), gpios);
+static const struct gpio_dt_spec user_btn = GPIO_DT_SPEC_GET(DT_ALIAS(user_btn), gpios);
+#endif /* DT_NODE_EXISTS(DT_ALIAS(golioth_led)) */
 
-#define CONSOLE_LABEL DT_LABEL(DT_CHOSEN(zephyr_console))
-#define OT_CONNECTION_LED DK_LED1
-
-int sensor_interval = 60;
-int counter = 0;
-
-struct device *temp_sensor;
-struct device *imu_sensor;
-struct device *mag_sensor;
+static struct gpio_callback button_cb_data;
 
 static struct k_work on_connect_work;
 static struct k_work on_disconnect_work;
 
-static struct golioth_client *client = GOLIOTH_SYSTEM_CLIENT_GET();
-
-static K_SEM_DEFINE(connected, 0, 1);
-
-
-static int sensor_push_handler(struct golioth_req_rsp *rsp)
-{
-	if (rsp->err) {
-		LOG_WRN("Failed to push sensor: %d", rsp->err);
-		return rsp->err;
-	}
-	dk_set_led_off(DK_LED2);
-	LOG_DBG("Sensor successfully pushed");
-	return 0;
-}
-
-// This work function will submit a LightDB Stream output
-// It should be called every time the sensor takes a reading
-
-void my_sensorstream_work_handler(struct k_work *work)
-{
-	int err;
-	struct sensor_value temp;
-	struct sensor_value accel_x;
-	struct sensor_value accel_y;
-	struct sensor_value accel_z;
-	struct sensor_value mag;
-	char sbuf[100];
-	
-	// kick off a temp sensor reading!
-	sensor_sample_fetch(temp_sensor);
-	sensor_channel_get(temp_sensor, SENSOR_CHAN_AMBIENT_TEMP, &temp);
-	LOG_DBG("Temp is %d.%06d", temp.val1, abs(temp.val2));
-
-	// kick off an IMU sensor reading!
-	sensor_sample_fetch(imu_sensor);
-	sensor_channel_get(imu_sensor, SENSOR_CHAN_ACCEL_X, &accel_x);
-	LOG_DBG("Accel X is %d.%06d", accel_x.val1, abs(accel_x.val2));
-	sensor_channel_get(imu_sensor, SENSOR_CHAN_ACCEL_Y, &accel_y);
-	LOG_DBG("Accel Y is %d.%06d", accel_y.val1, abs(accel_y.val2));	
-	sensor_channel_get(imu_sensor, SENSOR_CHAN_ACCEL_Z, &accel_z);
-	LOG_DBG("Accel Z is %d.%06d", accel_z.val1, abs(accel_z.val2));
-
-
-	// kick off a mag sensor reading!
-	sensor_sample_fetch(mag_sensor);
-	sensor_channel_get(mag_sensor, SENSOR_CHAN_PROX, &mag);
-	LOG_DBG("Mag is %d.%06d", mag.val1, abs(mag.val2));
-
-
-	snprintk(sbuf, sizeof(sbuf) - 1,
-			"{\"accel_x\":%f,\"accel_y\":%f,\"accel_z\":%f,\"mag\":%f,\"temp\":%f}",
-			sensor_value_to_double(&accel_x),
-			sensor_value_to_double(&accel_y),
-			sensor_value_to_double(&accel_z),
-			sensor_value_to_double(&mag),
-			sensor_value_to_double(&temp)
-			);
-
-	// Async send data to the cloud, get confirmation and call the push handler
-	err = golioth_stream_push_cb(client, "redSensor",
-				     GOLIOTH_CONTENT_FORMAT_APP_JSON,
-				     sbuf, strlen(sbuf),
-				     sensor_push_handler, NULL);
-	if (err) {
-		LOG_WRN("Failed to send sensor: %d", err);
-		printk("Failed to send sensor: %d\n", err);	
-	}
-
-}
-
-K_WORK_DEFINE(my_sensorstream_work, my_sensorstream_work_handler);
-
-
-// This work function initiates a sensor reading
-// And kick off a LightDB stream event
-// It should be called every time the timer fires
+/* forward declarations */
+void golioth_connection_led_set(uint8_t state);
 
-void my_sensor_work_handler(struct k_work *work)
+void wake_system_thread(void)
 {
-
-	LOG_DBG("LED on, taking sensor readings");
-	dk_set_led_on(DK_LED2);
-	k_work_submit(&my_sensorstream_work);
-	
+	k_wakeup(_system_thread);
 }
 
-K_WORK_DEFINE(my_sensor_work, my_sensor_work_handler);
-
-static int counter_set_handler(struct golioth_req_rsp *rsp)
+static void on_client_event(struct golioth_client *client,
+			    enum golioth_client_event event,
+			    void *arg)
 {
-	if (rsp->err) {
-		LOG_WRN("Failed to set counter: %d", rsp->err);
-		return rsp->err;
-	}
-
-	LOG_DBG("Counter successfully set");
-
-	return 0;
-}
-
+	bool is_connected = (event == GOLIOTH_CLIENT_EVENT_CONNECTED);
 
-void my_timer_handler(struct k_timer *dummy) {
-
-	char sbuf[sizeof("4294967295")];
-	int err;
-
-	snprintk(sbuf, sizeof(sbuf) - 1, "%d", counter);
-
-	LOG_INF("Interval of %d seconds is up, taking a reading", sensor_interval);
-	
-	err = golioth_lightdb_set_cb(client, "counter",
-				     GOLIOTH_CONTENT_FORMAT_APP_JSON,
-				     sbuf, strlen(sbuf),
-				     counter_set_handler, NULL);
-	if (err) {
-		LOG_WRN("Failed to set counter: %d", err);
-		return;
+	if (is_connected) {
+		k_sem_give(&connected);
+		golioth_connection_led_set(1);
+	} else {
+		golioth_connection_led_set(0);
 	}
 
-	counter++;
-
-
-	k_work_submit(&my_sensor_work);
-
-}
-
-K_TIMER_DEFINE(my_timer, my_timer_handler, NULL);
-
-
-static void golioth_on_connect(struct golioth_client *client)
-{
-	k_sem_give(&connected);
-
-	LOG_INF("Connected to Golioth!");
+	LOG_INF("Golioth client %s", is_connected ? "connected" : "disconnected");
 }
 
 static void on_ot_connect(struct k_work *item)
 {
 	ARG_UNUSED(item);
 
-	dk_set_led_off(OT_CONNECTION_LED);		// Turn LED off when connected
-	
+	LOG_INF("OpenThread on connect");
 }
 
 static void on_ot_disconnect(struct k_work *item)
 {
 	ARG_UNUSED(item);
 
-	dk_set_led_on(OT_CONNECTION_LED);		// Turn LED on when NOT connected
+	LOG_INF("OpenThread on disconnect");
 }
 
-
-static void on_button_changed(uint32_t button_state, uint32_t has_changed)
+static void start_golioth_client(void)
 {
-	uint32_t buttons = button_state & has_changed;
+	/* Get the client configuration from auto-loaded settings */
+	const struct golioth_client_config *client_config = golioth_sample_credentials_get();
 
-	if ((buttons & DK_BTN1_MSK) && button_state == 1) {
-		golioth_send_hello(client); 
-		LOG_DBG("Button %d pressed, taking a reading", has_changed);
-		k_work_submit(&my_sensor_work);
-	}
+	/* Create and start a Golioth Client */
+	client = golioth_client_create(client_config);
+
+	/* Register Golioth on_connect callback */
+	golioth_client_register_event_callback(client, on_client_event, NULL);
+
+	/* Initialize DFU components */
+	golioth_fw_update_init(client, _current_version);
+
+	/*** Call Golioth APIs for other services in dedicated app files ***/
+	/* Observe State service data */
+	app_state_observe(client);
+
+	/* Set Golioth Client for streaming sensor data */
+	app_sensors_set_client(client);
+
+	/* Register Settings service */
+	app_settings_register(client);
 
+	/* Register RPC service */
+	app_rpc_register(client);
 }
 
-static void on_thread_state_changed(uint32_t flags, void *context)
+static void on_thread_state_changed(otChangedFlags flags, struct openthread_context *ot_context,
+				    void *user_data)
 {
-	struct openthread_context *ot_context = context;
-
 	if (flags & OT_CHANGED_THREAD_ROLE) {
 		switch (otThreadGetDeviceRole(ot_context->instance)) {
 		case OT_DEVICE_ROLE_CHILD:
@@ -210,103 +120,92 @@ static void on_thread_state_changed(uint32_t flags, void *context)
 
 		case OT_DEVICE_ROLE_DISABLED:
 		case OT_DEVICE_ROLE_DETACHED:
+
 		default:
 			k_work_submit(&on_disconnect_work);
 			break;
 		}
 	}
-}
-
-void main(void)
-{
-	int ret;
-
-#if DT_NODE_HAS_COMPAT(DT_CHOSEN(zephyr_shell_uart), zephyr_cdc_acm_uart)
-	const struct device *dev;
-	uint32_t dtr = 0U;
-
-	ret = usb_enable(NULL);
-	if (ret != 0) {
-		LOG_ERR("Failed to enable USB");
-		return;
-	}
-
-	dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_shell_uart));
-	if (dev == NULL) {
-		LOG_ERR("Failed to find specific UART device");
-		return;
-	}
 
-	LOG_INF("Waiting for host to be ready to communicate");
-
-	/* Data Terminal Ready - check if host is ready to communicate */
-	while (!dtr) {
-		ret = uart_line_ctrl_get(dev, UART_LINE_CTRL_DTR, &dtr);
-		if (ret) {
-			LOG_ERR("Failed to get Data Terminal Ready line state: %d",
-				ret);
-			continue;
-		}
-		k_msleep(100);
+	if (flags == OT_CHANGED_IP6_ADDRESS_ADDED) {
+		start_golioth_client();
 	}
+}
 
-	/* Data Carrier Detect Modem - mark connection as established */
-	(void)uart_line_ctrl_set(dev, UART_LINE_CTRL_DCD, 1);
-	/* Data Set Ready - the NCP SoC is ready to communicate */
-	(void)uart_line_ctrl_set(dev, UART_LINE_CTRL_DSR, 1);
-#endif
-
-	LOG_INF("Start Golioth Thread sample");
-
-	ret = dk_buttons_init(on_button_changed);
-	if (ret) {
-		LOG_ERR("Cannot init buttons (error: %d)", ret);
-		return;
-	}
+static struct openthread_state_changed_cb ot_state_chaged_cb = {
+	.state_changed_cb = on_thread_state_changed
+};
 
-	ret = dk_leds_init();
-	if (ret) {
-		LOG_ERR("Cannot init leds, (error: %d)", ret);
-		return;
-	}
+void button_pressed(const struct device *dev, struct gpio_callback *cb, uint32_t pins)
+{
+	LOG_DBG("Button pressed at %d", k_cycle_get_32());
+	/* This function is an Interrupt Service Routine. Do not call functions that
+	 * use other threads, or perform long-running operations here
+	 */
+	k_wakeup(_system_thread);
+}
 
-	temp_sensor = (void *)DEVICE_DT_GET_ANY(silabs_si7055);
+/* Set (unset) LED indicators for active Golioth connection */
+void golioth_connection_led_set(uint8_t state)
+{
+	uint8_t pin_state = state ? 1 : 0;
+#if DT_NODE_EXISTS(DT_ALIAS(golioth_led))
+	/* Turn on Golioth logo LED once connected */
+	gpio_pin_set_dt(&golioth_led, pin_state);
+#endif /* #if DT_NODE_EXISTS(DT_ALIAS(golioth_led)) */
+	/* Change the state of the Golioth LED on Ostentus */
+	IF_ENABLED(CONFIG_LIB_OSTENTUS, (led_golioth_set(pin_state);));
+}
 
-    if (temp_sensor == NULL) {
-        printk("Could not get si7055 device\n");
-        return;
-    }
 
-	imu_sensor = (void *)DEVICE_DT_GET_ANY(st_lis2dh12);
+int main(void)
+{
+	int err = 0;
 
-    if (imu_sensor == NULL) {
-        printk("Could not get lis2dh12 device\n");
-        return;
-    }
+	LOG_DBG("Start OpenThread demo");
 
-	mag_sensor = (void *)DEVICE_DT_GET_ANY(honeywell_sm351lt);
+	IF_ENABLED(CONFIG_NET_L2_OPENTHREAD, (
+		k_work_init(&on_connect_work, on_ot_connect);
+		k_work_init(&on_disconnect_work, on_ot_disconnect);
 
-    if (mag_sensor == NULL) {
-        printk("Could not get sm351lt device\n");
-        return;
-    }
+		openthread_state_changed_cb_register(openthread_get_default_context(), &ot_state_chaged_cb);
+		openthread_start(openthread_get_default_context());
+	));
 
-	//Turn on LED 1 while connecting
-	dk_set_led_on(OT_CONNECTION_LED);
+	LOG_INF("Firmware version: %s", CONFIG_MCUBOOT_IMGTOOL_SIGN_VERSION);
 
-	k_work_init(&on_connect_work, on_ot_connect);
-	k_work_init(&on_disconnect_work, on_ot_disconnect);
+	/* Get system thread id so loop delay change event can wake main */
+	_system_thread = k_current_get();
 
-	openthread_set_state_changed_cb(on_thread_state_changed);
-	openthread_start(openthread_get_default_context());
+#if DT_NODE_EXISTS(DT_ALIAS(golioth_led))
+	/* Initialize Golioth logo LED */
+	err = gpio_pin_configure_dt(&golioth_led, GPIO_OUTPUT_INACTIVE);
+	if (err) {
+		LOG_ERR("Unable to configure LED for Golioth Logo");
+	}
+#endif /* #if DT_NODE_EXISTS(DT_ALIAS(golioth_led)) */
 
-	client->on_connect = golioth_on_connect;
-	golioth_system_client_start();
+	/* Set up user button */
+	err = gpio_pin_configure_dt(&user_btn, GPIO_INPUT);
+	if (err) {
+		LOG_ERR("Error %d: failed to configure %s pin %d", err, user_btn.port->name,
+			user_btn.pin);
+		return err;
+	}
 
-	k_sem_take(&connected, K_FOREVER);
+	err = gpio_pin_interrupt_configure_dt(&user_btn, GPIO_INT_EDGE_TO_ACTIVE);
+	if (err) {
+		LOG_ERR("Error %d: failed to configure interrupt on %s pin %d", err,
+			user_btn.port->name, user_btn.pin);
+		return err;
+	}
 
-	dk_set_led_off(DK_LED2);
+	gpio_init_callback(&button_cb_data, button_pressed, BIT(user_btn.pin));
+	gpio_add_callback(user_btn.port, &button_cb_data);
 
-    k_timer_start(&my_timer, K_SECONDS(sensor_interval), K_SECONDS(sensor_interval));
+	while (true) {
+		app_sensors_read_and_stream();
 
-}
\ No newline at end of file
+		k_sleep(K_SECONDS(get_loop_delay_s()));
+	}
+}
diff --git a/src/main.h b/src/main.h
new file mode 100644
index 0000000..0737d46
--- /dev/null
+++ b/src/main.h
@@ -0,0 +1,7 @@
+/*
+ * Copyright (c) 2023 Golioth, Inc.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+void wake_system_thread(void);
diff --git a/usb.overlay b/usb.overlay
deleted file mode 100644
index 50a8193..0000000
--- a/usb.overlay
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
- * Copyright (c) 2021 Nordic Semiconductor ASA
- *
- * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
- */
-
-/ {
-	chosen {
-		zephyr,shell-uart = &cdc_acm_uart0;
-	};
-};
-
-&zephyr_udc0 {
-	cdc_acm_uart0: cdc_acm_uart0 {
-		compatible = "zephyr,cdc-acm-uart";
-		label = "CDC_ACM_0";
-	};
-};
diff --git a/west.yml b/west.yml
index a13a36d..0039389 100644
--- a/west.yml
+++ b/west.yml
@@ -1,49 +1,37 @@
+# Copyright (c) 2022-2023 Golioth, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
 manifest:
-  version: 0.7
- 
-  defaults:
-    remote: nrfconnect
+  version: 0.8
 
-  remotes:
-    - name: nrfconnect
-      url-base: https://github.com/nrfconnect
   projects:
-    - name: nrf
-      repo-path: sdk-nrf
-      remote: nrfconnect
-      revision: v2.1.0
+    - name: golioth
+      path: modules/lib/golioth-firmware-sdk
+      revision: v0.13.1
+      url: https://github.com/golioth/golioth-firmware-sdk.git
+      west-commands: scripts/west-commands.yml
+      submodules: true
       import:
+        file: west-ncs.yml
         path-prefix: deps
         name-allowlist:
+          - nrf
           - zephyr
           - cmsis
           - hal_nordic
           - mbedtls
+          - mbedtls-nrf
+          - mcuboot
           - net-tools
-          - nrf_hw_models
-          - segger
-          - tinycrypt
-          - tf-m-tests
           - nrfxlib
-          - mcuboot
-          - mcumgr
-          - tinycbor
-          - mbedtls-nrf
-          - memfault-firmware-sdk
           - openthread
-
-    # Golioth repository.
-    - name: golioth
-      path: deps/modules/lib/golioth
-      revision: v0.5.0
-      url: https://github.com/golioth/zephyr-sdk.git
-
-    # QCBOR
-    - name: qcbor
-      path: deps/modules/lib/qcbor
-      revision: 17b5607b8c49b835d22dec3effa97b25c89267b3
-      url: https://github.com/golioth/QCBOR.git
+          - qcbor
+          - segger
+          - tfm-mcuboot
+          - tinycrypt
+          - trusted-firmware-m
+          - zcbor
 
   self:
     path: app
-    west-commands: scripts/west-commands.yml
+